git ssb

3+

ev / decent



Commit cc82650a85e0b733ac8fe1c96af74e69231faf4c

three different gossip algos

Ev Bogue committed on 9/10/2017, 12:07:58 AM
Parent: 9729cbbd473487282f0ef7b486cb87adb8c622f8

Files changed

decent.jschanged
plugins/ssb-config/inject.jschanged
plugins/classicgossip/index.jsadded
plugins/classicgossip/init.jsadded
plugins/classicgossip/schedule.jsadded
plugins/gossip/index.jsdeleted
plugins/gossip/init.jsdeleted
plugins/gossip/schedule.jsdeleted
plugins/hackedgossip/index.jsadded
plugins/hackedgossip/init.jsadded
plugins/hackedgossip/schedule.jsadded
plugins/moderngossip/index.jsadded
plugins/moderngossip/init.jsadded
plugins/moderngossip/schedule.jsadded
decent.jsView
@@ -11,9 +11,9 @@
1111 var manifestFile = path.join(config.path, 'manifest.json')
1212
1313 var createSbot = require('./lib')
1414 .use(require('./plugins/master'))
15- .use(require('./plugins/gossip'))
15 + .use(require('./plugins/moderngossip'))
1616 .use(require('./plugins/replicate'))
1717 .use(require('ssb-friends'))
1818 .use(require('ssb-blobs'))
1919 .use(require('./plugins/local'))
plugins/ssb-config/inject.jsView
@@ -9,11 +9,12 @@
99 var SEC = 1e3
1010 var MIN = 60*SEC
1111
1212 module.exports = function (name, override) {
13 + //name = name || 'ssb'
1314 name = name || 'decent'
1415 var HOME = home() || 'browser' //most probably browser
15- return RC(name || 'decent', merge({
16 + return RC(name, merge({
1617 //just use an ipv4 address by default.
1718 //there have been some reports of seemingly non-private
1819 //ipv6 addresses being returned and not working.
1920 //https://github.com/ssbc/scuttlebot/pull/102
@@ -45,9 +46,9 @@
4546 //this will be updated whenever breaking changes are made.
4647 //(see secret-handshake paper for a full explaination)
4748 //(generated by crypto.randomBytes(32).toString('base64'))
4849 shs: 'EVRctE2Iv8GrO/BpQCF34e2FMPsDJot9x0j846LjVtc=',
49-
50 + //shs: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=',
5051 //used to sign messages
5152 sign: null
5253 },
5354 master: [],
plugins/classicgossip/index.jsView
@@ -1,0 +1,230 @@
1 +'use strict'
2 +var pull = require('pull-stream')
3 +var Notify = require('pull-notify')
4 +var ref = require('ssb-ref')
5 +var mdm = require('mdmanifest')
6 +var valid = require('../../lib/validators')
7 +var apidoc = require('../../lib/apidocs').gossip
8 +var u = require('../../lib/util')
9 +var ping = require('pull-ping')
10 +var stats = require('statistics')
11 +var Schedule = require('./schedule')
12 +var Init = require('./init')
13 +
14 +function isFunction (f) {
15 + return 'function' === typeof f
16 +}
17 +
18 +function stringify(peer) {
19 + return [peer.host, peer.port, peer.key].join(':')
20 +}
21 +
22 +/*
23 +Peers : [{
24 + key: id,
25 + host: ip,
26 + port: int,
27 + //to be backwards compatible with patchwork...
28 + announcers: {length: int}
29 + source: 'pub'|'manual'|'local'
30 +}]
31 +*/
32 +
33 +module.exports = {
34 + name: 'gossip',
35 + version: '1.0.0',
36 + manifest: mdm.manifest(apidoc),
37 + permissions: {
38 + anonymous: {allow: ['ping']}
39 + },
40 + init: function (server, config) {
41 + var notify = Notify()
42 + var conf = config.gossip || {}
43 + var home = ref.parseAddress(server.getAddress())
44 +
45 + //Known Peers
46 + var peers = []
47 +
48 + function getPeer(id) {
49 + return u.find(peers, function (e) {
50 + return e && e.key === id
51 + })
52 + }
53 +
54 + var timer_ping = 5*6e4
55 +
56 + var gossip = {
57 + wakeup: 0,
58 + peers: function () {
59 + return peers
60 + },
61 + get: function (addr) {
62 + addr = ref.parseAddress(addr)
63 + return u.find(peers, function (a) {
64 + return (
65 + addr.port === a.port
66 + && addr.host === a.host
67 + && addr.key === a.key
68 + )
69 + })
70 + },
71 + connect: valid.async(function (addr, cb) {
72 + addr = ref.parseAddress(addr)
73 + if (!addr || typeof addr != 'object')
74 + return cb(new Error('first param must be an address'))
75 +
76 + if(!addr.key) return cb(new Error('address must have ed25519 key'))
77 + // add peer to the table, incase it isn't already.
78 + gossip.add(addr, 'manual')
79 + var p = gossip.get(addr)
80 + if(!p) return cb()
81 +
82 + p.stateChange = Date.now()
83 + p.state = 'connecting'
84 + server.connect(p, function (err, rpc) {
85 + if (err) {
86 + p.state = undefined
87 + p.failure = (p.failure || 0) + 1
88 + p.stateChange = Date.now()
89 + notify({ type: 'connect-failure', peer: p })
90 + server.emit('log:info', ['SBOT', p.host+':'+p.port+p.key, 'connection failed', err.message || err])
91 + p.duration = stats(p.duration, 0)
92 + return (cb && cb(err))
93 + }
94 + else {
95 + p.state = 'connected'
96 + p.failure = 0
97 + }
98 + cb && cb(null, rpc)
99 + })
100 +
101 + }, 'string|object'),
102 +
103 + disconnect: valid.async(function (addr, cb) {
104 + var peer = this.get(addr)
105 +
106 + peer.state = 'disconnecting'
107 + peer.stateChange = Date.now()
108 + if(!peer || !peer.disconnect) cb && cb()
109 + else peer.disconnect(null, function (err) {
110 + peer.stateChange = Date.now()
111 + })
112 +
113 + }, 'string|object'),
114 +
115 + changes: function () {
116 + return notify.listen()
117 + },
118 + //add an address to the peer table.
119 + add: valid.sync(function (addr, source) {
120 + addr = ref.parseAddress(addr)
121 + if(!ref.isAddress(addr))
122 + throw new Error('not a valid address:' + JSON.stringify(addr))
123 + // check that this is a valid address, and not pointing at self.
124 +
125 + if(addr.key === home.key) return
126 + if(addr.host === home.host && addr.port === home.port) return
127 +
128 + var f = gossip.get(addr)
129 +
130 + if(!f) {
131 + // new peer
132 + addr.source = source
133 + addr.announcers = 1
134 + addr.duration = addr.duration || null
135 + peers.push(addr)
136 + notify({ type: 'discover', peer: addr, source: source || 'manual' })
137 + return addr
138 + }
139 + //don't count local over and over
140 + else if(f.source != 'local')
141 + f.announcers ++
142 +
143 + return f
144 + }, 'string|object', 'string?'),
145 + ping: function (opts) {
146 + var timeout = config.timers && config.timers.ping || 5*60e3
147 + //between 10 seconds and 30 minutes, default 5 min
148 + timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
149 + return ping({timeout: timeout})
150 + },
151 + reconnect: function () {
152 + for(var id in server.peers)
153 + if(id !== server.id) //don't disconnect local client
154 + server.peers[id].forEach(function (peer) {
155 + peer.close(true)
156 + })
157 + return gossip.wakeup = Date.now()
158 + }
159 + }
160 +
161 + Schedule (gossip, config, server)
162 + Init (gossip, config, server)
163 + //get current state
164 +
165 + server.on('rpc:connect', function (rpc, isClient) {
166 + var peer = getPeer(rpc.id)
167 + //don't track clients that connect, but arn't considered peers.
168 + //maybe we should though?
169 + if(!peer) return
170 + console.log('Connected', stringify(peer))
171 + //means that we have created this connection, not received it.
172 + peer.client = !!isClient
173 + peer.state = 'connected'
174 + peer.stateChange = Date.now()
175 + peer.disconnect = function (err, cb) {
176 + if(isFunction(err)) cb = err, err = null
177 + rpc.close(err, cb)
178 + }
179 +
180 + if(isClient) {
181 + //default ping is 5 minutes...
182 + var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
183 + peer.ping = {rtt: pp.rtt, skew: pp.skew}
184 + pull(
185 + pp,
186 + rpc.gossip.ping({timeout: timer_ping}, function (err) {
187 + if(err.name === 'TypeError') peer.ping.fail = true
188 + }),
189 + pp
190 + )
191 + }
192 +
193 + rpc.on('closed', function () {
194 + console.log('Disconnected', stringify(peer))
195 + //track whether we have successfully connected.
196 + //or how many failures there have been.
197 + var since = peer.stateChange
198 + peer.stateChange = Date.now()
199 + // if(peer.state === 'connected') //may be "disconnecting"
200 + // peer.duration.value(peer.stateChange - since)
201 + peer.duration = stats(peer.duration, peer.stateChange - since)
202 + peer.state = undefined
203 + notify({ type: 'disconnect', peer: peer })
204 + server.emit('log:info', ['SBOT', rpc.id, 'disconnect'])
205 + })
206 +
207 + notify({ type: 'connect', peer: peer })
208 + })
209 +
210 + return gossip
211 + }
212 +}
213 +
214 +
215 +
216 +
217 +
218 +
219 +
220 +
221 +
222 +
223 +
224 +
225 +
226 +
227 +
228 +
229 +
230 +
plugins/classicgossip/init.jsView
@@ -1,0 +1,38 @@
1 +//var isArray = Array.isArray
2 +var pull = require('pull-stream')
3 +var ref = require('ssb-ref')
4 +
5 +module.exports = function (gossip, config, server) {
6 +
7 + // populate peertable with configured seeds (mainly used in testing)
8 + //var seeds = config.seeds
9 + //
10 + //;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
11 + //.forEach(function (addr) { gossip.add(addr, 'seed') })
12 +
13 + // populate peertable with pub announcements on the feed
14 + pull(
15 + server.messagesByType({
16 + type: 'pub', live: true, keys: false
17 + }),
18 + //pull.drain(function (msg) {
19 + // if(!msg.content.address) return
20 + // gossip.add(msg.content.address, 'pub')
21 + //})
22 + pull.drain(function (msg) {
23 + if(msg.sync) return
24 + if(!msg.content.address) return
25 + if(ref.isAddress(msg.content.address))
26 + gossip.add(msg.content.address, 'pub')
27 + })
28 + )
29 +
30 + // populate peertable with announcements on the LAN multicast
31 + server.on('local', function (_peer) {
32 + gossip.add(_peer, 'local')
33 + })
34 +
35 +}
36 +
37 +
38 +
plugins/classicgossip/schedule.jsView
@@ -1,0 +1,169 @@
1 +var ip = require('ip')
2 +var onWakeup = require('on-wakeup')
3 +var onNetwork = require('on-change-network')
4 +var hasNetwork = require('has-network')
5 +var pull = require('pull-stream')
6 +
7 +function stringify(peer) {
8 + return [peer.host, peer.port, peer.key].join(':')
9 +}
10 +
11 +function not (fn) {
12 + return function (e) { return !fn(e) }
13 +}
14 +
15 +function and () {
16 + var args = [].slice.call(arguments)
17 + return function (value) {
18 + return args.every(function (fn) { return fn.call(null, value) })
19 + }
20 +}
21 +
22 +function delay (failures, factor, max) {
23 + return Math.min(Math.pow(2, failures)*factor, max || Infinity)
24 +}
25 +
26 +function maxStateChange (M, e) {
27 + return Math.max(M, e.stateChange || 0)
28 +}
29 +
30 +function peerNext(peer, opts) {
31 + return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
32 +}
33 +
34 +function isOffline (e) {
35 + if(ip.isLoopback(e.host)) return false
36 + return !hasNetwork()
37 +}
38 +
39 +var isOnline = not(isOffline)
40 +
41 +function isLocal (e) {
42 + return ip.isPrivate(e.host) && e.type === 'local'
43 +}
44 +
45 +function isUnattempted (e) {
46 + return !e.stateChange
47 +}
48 +
49 +function isInactive (e) {
50 + return e.state !== 'connecting' && e.stateChange && (!e.duration || e.duration.mean == 0)
51 +}
52 +
53 +function isLongterm (e) {
54 + return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
55 +}
56 +
57 +function isLegacy (peer) {
58 + return peer.duration && (peer.duration && peer.duration.mean > 0) && !exports.isLongterm(peer)
59 +}
60 +
61 +function isConnect (e) {
62 + return 'connected' === e.state || 'connecting' === e.state
63 +}
64 +
65 +function earliest(peers, n) {
66 + return peers.sort(function (a, b) {
67 + return a.stateChange - b.stateChange
68 + }).slice(0, Math.max(n, 0))
69 +}
70 +
71 +function select(peers, ts, filter, opts) {
72 + if(opts.disable) return []
73 + //opts: { quota, groupMin, min, factor, max }
74 + var type = peers.filter(filter)
75 + var unconnect = type.filter(not(isConnect))
76 + var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
77 + var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
78 + if(ts < min) return []
79 +
80 + return earliest(unconnect.filter(function (peer) {
81 + return peerNext(peer, opts) < ts
82 + }), count)
83 +}
84 +
85 +var schedule = exports = module.exports =
86 +function (gossip, config, server) {
87 +
88 + var min = 60e3, hour = 60*60e3
89 +
90 + onWakeup(gossip.reconnect)
91 + onNetwork(gossip.reconnect)
92 +
93 + function conf(name, def) {
94 + if(!config.gossip) return def
95 + var value = config.gossip[name]
96 + return (value === undefined || value === '') ? def : value
97 + }
98 +
99 + function connect (peers, ts, name, filter, opts) {
100 + var connected = peers.filter(isConnect).filter(filter)
101 + .filter(function (peer) {
102 + return peer.stateChange + 10e3 < ts
103 + })
104 +
105 + if(connected.length > opts.quota) {
106 + return earliest(connected, connected.length - opts.quota)
107 + .forEach(function (peer) {
108 + console.log('Disconnect', name, stringify(peer))
109 + gossip.disconnect(peer)
110 + })
111 + }
112 +
113 + select(peers, ts, and(filter, isOnline), opts)
114 + .forEach(function (peer) {
115 + console.log('-Connect', name, stringify(peer))
116 + gossip.connect(peer)
117 + })
118 + }
119 + function connections () {
120 + var ts = Date.now()
121 + var peers = gossip.peers()
122 +
123 + //quota, groupMin, min, factor, max
124 +
125 + connect(peers, ts, 'attempt', exports.isUnattempted, {
126 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
127 + disable: !conf('global', true)
128 + })
129 +
130 + connect(peers, ts, 'retry', exports.isInactive, {
131 + min: 0, quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3
132 + })
133 +
134 + connect(peers, ts, 'legacy', exports.isLegacy, {
135 + quota: 3, factor: 5*min, max: 3*hour, groupMin: 5*min,
136 + disable: !conf('global', true)
137 + })
138 +
139 + connect(peers, ts, 'longterm', exports.isLongterm, {
140 + quota: 3, factor: 10e3, max: 10*min, groupMin: 5e3,
141 + disable: !conf('global', true)
142 + })
143 +
144 + connect(peers, ts, 'local', exports.isLocal, {
145 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
146 + disable: !conf('local', true)
147 + })
148 + }
149 +
150 + pull(
151 + gossip.changes(),
152 + pull.drain(function (ev) {
153 + if(ev.type == 'disconnect')
154 + connections()
155 + })
156 + )
157 +
158 + var int = setInterval(connections, 2e3)
159 +
160 + connections()
161 +}
162 +
163 +exports.isUnattempted = isUnattempted
164 +exports.isInactive = isInactive
165 +exports.isLongterm = isLongterm
166 +exports.isLegacy = isLegacy
167 +exports.isLocal = isLocal
168 +exports.isConnectedOrConnecting = isConnect
169 +exports.select = select
plugins/gossip/index.jsView
@@ -1,230 +1,0 @@
1-'use strict'
2-var pull = require('pull-stream')
3-var Notify = require('pull-notify')
4-var ref = require('ssb-ref')
5-var mdm = require('mdmanifest')
6-var valid = require('../../lib/validators')
7-var apidoc = require('../../lib/apidocs').gossip
8-var u = require('../../lib/util')
9-var ping = require('pull-ping')
10-var stats = require('statistics')
11-var Schedule = require('./schedule')
12-var Init = require('./init')
13-
14-function isFunction (f) {
15- return 'function' === typeof f
16-}
17-
18-function stringify(peer) {
19- return [peer.host, peer.port, peer.key].join(':')
20-}
21-
22-/*
23-Peers : [{
24- key: id,
25- host: ip,
26- port: int,
27- //to be backwards compatible with patchwork...
28- announcers: {length: int}
29- source: 'pub'|'manual'|'local'
30-}]
31-*/
32-
33-module.exports = {
34- name: 'gossip',
35- version: '1.0.0',
36- manifest: mdm.manifest(apidoc),
37- permissions: {
38- anonymous: {allow: ['ping']}
39- },
40- init: function (server, config) {
41- var notify = Notify()
42- var conf = config.gossip || {}
43- var home = ref.parseAddress(server.getAddress())
44-
45- //Known Peers
46- var peers = []
47-
48- function getPeer(id) {
49- return u.find(peers, function (e) {
50- return e && e.key === id
51- })
52- }
53-
54- var timer_ping = 5*6e4
55-
56- var gossip = {
57- wakeup: 0,
58- peers: function () {
59- return peers
60- },
61- get: function (addr) {
62- addr = ref.parseAddress(addr)
63- return u.find(peers, function (a) {
64- return (
65- addr.port === a.port
66- && addr.host === a.host
67- && addr.key === a.key
68- )
69- })
70- },
71- connect: valid.async(function (addr, cb) {
72- addr = ref.parseAddress(addr)
73- if (!addr || typeof addr != 'object')
74- return cb(new Error('first param must be an address'))
75-
76- if(!addr.key) return cb(new Error('address must have ed25519 key'))
77- // add peer to the table, incase it isn't already.
78- gossip.add(addr, 'manual')
79- var p = gossip.get(addr)
80- if(!p) return cb()
81-
82- p.stateChange = Date.now()
83- p.state = 'connecting'
84- server.connect(p, function (err, rpc) {
85- if (err) {
86- p.state = undefined
87- p.failure = (p.failure || 0) + 1
88- p.stateChange = Date.now()
89- notify({ type: 'connect-failure', peer: p })
90- server.emit('log:info', ['SBOT', p.host+':'+p.port+p.key, 'connection failed', err.message || err])
91- p.duration = stats(p.duration, 0)
92- return (cb && cb(err))
93- }
94- else {
95- p.state = 'connected'
96- p.failure = 0
97- }
98- cb && cb(null, rpc)
99- })
100-
101- }, 'string|object'),
102-
103- disconnect: valid.async(function (addr, cb) {
104- var peer = this.get(addr)
105-
106- peer.state = 'disconnecting'
107- peer.stateChange = Date.now()
108- if(!peer || !peer.disconnect) cb && cb()
109- else peer.disconnect(null, function (err) {
110- peer.stateChange = Date.now()
111- })
112-
113- }, 'string|object'),
114-
115- changes: function () {
116- return notify.listen()
117- },
118- //add an address to the peer table.
119- add: valid.sync(function (addr, source) {
120- addr = ref.parseAddress(addr)
121- if(!ref.isAddress(addr))
122- throw new Error('not a valid address:' + JSON.stringify(addr))
123- // check that this is a valid address, and not pointing at self.
124-
125- if(addr.key === home.key) return
126- if(addr.host === home.host && addr.port === home.port) return
127-
128- var f = gossip.get(addr)
129-
130- if(!f) {
131- // new peer
132- addr.source = source
133- addr.announcers = 1
134- addr.duration = addr.duration || null
135- peers.push(addr)
136- notify({ type: 'discover', peer: addr, source: source || 'manual' })
137- return addr
138- }
139- //don't count local over and over
140- else if(f.source != 'local')
141- f.announcers ++
142-
143- return f
144- }, 'string|object', 'string?'),
145- ping: function (opts) {
146- var timeout = config.timers && config.timers.ping || 5*60e3
147- //between 10 seconds and 30 minutes, default 5 min
148- timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
149- return ping({timeout: timeout})
150- },
151- reconnect: function () {
152- for(var id in server.peers)
153- if(id !== server.id) //don't disconnect local client
154- server.peers[id].forEach(function (peer) {
155- peer.close(true)
156- })
157- return gossip.wakeup = Date.now()
158- }
159- }
160-
161- Schedule (gossip, config, server)
162- Init (gossip, config, server)
163- //get current state
164-
165- server.on('rpc:connect', function (rpc, isClient) {
166- var peer = getPeer(rpc.id)
167- //don't track clients that connect, but arn't considered peers.
168- //maybe we should though?
169- if(!peer) return
170- console.log('Connected', stringify(peer))
171- //means that we have created this connection, not received it.
172- peer.client = !!isClient
173- peer.state = 'connected'
174- peer.stateChange = Date.now()
175- peer.disconnect = function (err, cb) {
176- if(isFunction(err)) cb = err, err = null
177- rpc.close(err, cb)
178- }
179-
180- if(isClient) {
181- //default ping is 5 minutes...
182- var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
183- peer.ping = {rtt: pp.rtt, skew: pp.skew}
184- pull(
185- pp,
186- rpc.gossip.ping({timeout: timer_ping}, function (err) {
187- if(err.name === 'TypeError') peer.ping.fail = true
188- }),
189- pp
190- )
191- }
192-
193- rpc.on('closed', function () {
194- console.log('Disconnected', stringify(peer))
195- //track whether we have successfully connected.
196- //or how many failures there have been.
197- var since = peer.stateChange
198- peer.stateChange = Date.now()
199- // if(peer.state === 'connected') //may be "disconnecting"
200- // peer.duration.value(peer.stateChange - since)
201- peer.duration = stats(peer.duration, peer.stateChange - since)
202- peer.state = undefined
203- notify({ type: 'disconnect', peer: peer })
204- server.emit('log:info', ['SBOT', rpc.id, 'disconnect'])
205- })
206-
207- notify({ type: 'connect', peer: peer })
208- })
209-
210- return gossip
211- }
212-}
213-
214-
215-
216-
217-
218-
219-
220-
221-
222-
223-
224-
225-
226-
227-
228-
229-
230-
plugins/gossip/init.jsView
@@ -1,38 +1,0 @@
1-//var isArray = Array.isArray
2-var pull = require('pull-stream')
3-var ref = require('ssb-ref')
4-
5-module.exports = function (gossip, config, server) {
6-
7- // populate peertable with configured seeds (mainly used in testing)
8- //var seeds = config.seeds
9- //
10- //;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
11- //.forEach(function (addr) { gossip.add(addr, 'seed') })
12-
13- // populate peertable with pub announcements on the feed
14- pull(
15- server.messagesByType({
16- type: 'pub', live: true, keys: false
17- }),
18- //pull.drain(function (msg) {
19- // if(!msg.content.address) return
20- // gossip.add(msg.content.address, 'pub')
21- //})
22- pull.drain(function (msg) {
23- if(msg.sync) return
24- if(!msg.content.address) return
25- if(ref.isAddress(msg.content.address))
26- gossip.add(msg.content.address, 'pub')
27- })
28- )
29-
30- // populate peertable with announcements on the LAN multicast
31- server.on('local', function (_peer) {
32- gossip.add(_peer, 'local')
33- })
34-
35-}
36-
37-
38-
plugins/gossip/schedule.jsView
@@ -1,169 +1,0 @@
1-var ip = require('ip')
2-var onWakeup = require('on-wakeup')
3-var onNetwork = require('on-change-network')
4-var hasNetwork = require('has-network')
5-var pull = require('pull-stream')
6-
7-function stringify(peer) {
8- return [peer.host, peer.port, peer.key].join(':')
9-}
10-
11-function not (fn) {
12- return function (e) { return !fn(e) }
13-}
14-
15-function and () {
16- var args = [].slice.call(arguments)
17- return function (value) {
18- return args.every(function (fn) { return fn.call(null, value) })
19- }
20-}
21-
22-function delay (failures, factor, max) {
23- return Math.min(Math.pow(2, failures)*factor, max || Infinity)
24-}
25-
26-function maxStateChange (M, e) {
27- return Math.max(M, e.stateChange || 0)
28-}
29-
30-function peerNext(peer, opts) {
31- return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
32-}
33-
34-function isOffline (e) {
35- if(ip.isLoopback(e.host)) return false
36- return !hasNetwork()
37-}
38-
39-var isOnline = not(isOffline)
40-
41-function isLocal (e) {
42- return ip.isPrivate(e.host) && e.type === 'local'
43-}
44-
45-function isUnattempted (e) {
46- return !e.stateChange
47-}
48-
49-function isInactive (e) {
50- return e.state !== 'connecting' && e.stateChange && (!e.duration || e.duration.mean == 0)
51-}
52-
53-function isLongterm (e) {
54- return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
55-}
56-
57-function isLegacy (peer) {
58- return peer.duration && (peer.duration && peer.duration.mean > 0) && !exports.isLongterm(peer)
59-}
60-
61-function isConnect (e) {
62- return 'connected' === e.state || 'connecting' === e.state
63-}
64-
65-function earliest(peers, n) {
66- return peers.sort(function (a, b) {
67- return a.stateChange - b.stateChange
68- }).slice(0, Math.max(n, 0))
69-}
70-
71-function select(peers, ts, filter, opts) {
72- if(opts.disable) return []
73- //opts: { quota, groupMin, min, factor, max }
74- var type = peers.filter(filter)
75- var unconnect = type.filter(not(isConnect))
76- var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
77- var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
78- if(ts < min) return []
79-
80- return earliest(unconnect.filter(function (peer) {
81- return peerNext(peer, opts) < ts
82- }), count)
83-}
84-
85-var schedule = exports = module.exports =
86-function (gossip, config, server) {
87-
88- var min = 60e3, hour = 60*60e3
89-
90- onWakeup(gossip.reconnect)
91- onNetwork(gossip.reconnect)
92-
93- function conf(name, def) {
94- if(!config.gossip) return def
95- var value = config.gossip[name]
96- return (value === undefined || value === '') ? def : value
97- }
98-
99- function connect (peers, ts, name, filter, opts) {
100- var connected = peers.filter(isConnect).filter(filter)
101- .filter(function (peer) {
102- return peer.stateChange + 10e3 < ts
103- })
104-
105- if(connected.length > opts.quota) {
106- return earliest(connected, connected.length - opts.quota)
107- .forEach(function (peer) {
108- console.log('Disconnect', name, stringify(peer))
109- gossip.disconnect(peer)
110- })
111- }
112-
113- select(peers, ts, and(filter, isOnline), opts)
114- .forEach(function (peer) {
115- console.log('-Connect', name, stringify(peer))
116- gossip.connect(peer)
117- })
118- }
119- function connections () {
120- var ts = Date.now()
121- var peers = gossip.peers()
122-
123- //quota, groupMin, min, factor, max
124-
125- connect(peers, ts, 'attempt', exports.isUnattempted, {
126- min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
127- disable: !conf('global', true)
128- })
129-
130- connect(peers, ts, 'retry', exports.isInactive, {
131- min: 0, quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3
132- })
133-
134- connect(peers, ts, 'legacy', exports.isLegacy, {
135- quota: 3, factor: 5*min, max: 3*hour, groupMin: 5*min,
136- disable: !conf('global', true)
137- })
138-
139- connect(peers, ts, 'longterm', exports.isLongterm, {
140- quota: 3, factor: 10e3, max: 10*min, groupMin: 5e3,
141- disable: !conf('global', true)
142- })
143-
144- connect(peers, ts, 'local', exports.isLocal, {
145- quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
146- disable: !conf('local', true)
147- })
148- }
149-
150- pull(
151- gossip.changes(),
152- pull.drain(function (ev) {
153- if(ev.type == 'disconnect')
154- connections()
155- })
156- )
157-
158- var int = setInterval(connections, 2e3)
159-
160- connections()
161-}
162-
163-exports.isUnattempted = isUnattempted
164-exports.isInactive = isInactive
165-exports.isLongterm = isLongterm
166-exports.isLegacy = isLegacy
167-exports.isLocal = isLocal
168-exports.isConnectedOrConnecting = isConnect
169-exports.select = select
plugins/hackedgossip/index.jsView
@@ -1,0 +1,367 @@
1 +'use strict'
2 +var pull = require('pull-stream')
3 +var Notify = require('pull-notify')
4 +var valid = require('../../lib/validators')
5 +var u = require('../../lib/util')
6 +var ref = require('ssb-ref')
7 +//var ping = require('pull-ping')
8 +var stats = require('statistics')
9 +var Schedule = require('./schedule')
10 +var Init = require('./init')
11 +var AtomicFile = require('atomic-file')
12 +var fs = require('fs')
13 +var path = require('path')
14 +var deepEqual = require('deep-equal')
15 +
16 +function isFunction (f) {
17 + return 'function' === typeof f
18 +}
19 +
20 +function stringify(peer) {
21 + return [peer.host, peer.port, peer.key].join(':')
22 +}
23 +
24 +function isObject (o) {
25 + return o && 'object' == typeof o
26 +}
27 +
28 +function toBase64 (s) {
29 + if(isString(s)) return s
30 + else s.toString('base64') //assume a buffer
31 +}
32 +
33 +function isString (s) {
34 + return 'string' == typeof s
35 +}
36 +
37 +/*function coearseAddress (address) {
38 + if(isObject(address)) {
39 + var protocol = 'net'
40 + if (address.host.endsWith(".onion"))
41 + protocol = 'onion'
42 + return [protocol, address.host, address.port].join(':') +'~'+['shs', toBase64(address.key)].join(':')
43 + }
44 + return address
45 +}*/
46 +
47 +/*
48 +Peers : [{
49 + key: id,
50 + host: ip,
51 + port: int,
52 + //to be backwards compatible with patchwork...
53 + announcers: {length: int}
54 + source: 'pub'|'manual'|'local'
55 +}]
56 +*/
57 +
58 +
59 +module.exports = {
60 + name: 'gossip',
61 + version: '1.0.0',
62 + manifest: {},
63 + init: function (server, config) {
64 + var notify = Notify()
65 + var closed = false, closeScheduler
66 + var conf = config.gossip || {}
67 + var home = ref.parseAddress(server.getAddress())
68 +
69 + var stateFile = AtomicFile(path.join(config.path, 'gossip.json'))
70 +
71 + var status = {}
72 +
73 + //Known Peers
74 + var peers = []
75 +
76 + function getPeer(id) {
77 + return u.find(peers, function (e) {
78 + return e && e.key === id
79 + })
80 + }
81 +
82 + function simplify (peer) {
83 + return {
84 + address: /*coearseAddress(peer)*/peer,
85 + source: peer.source,
86 + state: peer.state, stateChange: peer.stateChange,
87 + failure: peer.failure,
88 + client: peer.client,
89 + //stats: {
90 + // duration: peer.duration || undefined,
91 + // rtt: peer.ping ? peer.ping.rtt : undefined,
92 + // skew: peer.ping ? peer.ping.skew : undefined,
93 + //}
94 + }
95 + }
96 +
97 + server.status.hook(function (fn) {
98 + var _status = fn()
99 + _status.gossip = status
100 + peers.forEach(function (peer) {
101 + if(peer.stateChange + 3e3 > Date.now() || peer.state === 'connected')
102 + status[peer.key] = simplify(peer)
103 + })
104 + return _status
105 +
106 + })
107 +
108 + server.close.hook(function (fn, args) {
109 + closed = true
110 + closeScheduler()
111 + for(var id in server.peers)
112 + server.peers[id].forEach(function (peer) {
113 + peer.close(true)
114 + })
115 + return fn.apply(this, args)
116 + })
117 +
118 + var timer_ping = 5*6e4
119 +
120 + function setConfig(name, value) {
121 + config.gossip = config.gossip || {}
122 + config.gossip[name] = value
123 +
124 + var cfgPath = path.join(config.path, 'config')
125 + var existingConfig = {}
126 +
127 + // load ~/.ssb/config
128 + try { existingConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) }
129 + catch (e) {}
130 +
131 + // update the plugins config
132 + existingConfig.gossip = existingConfig.gossip || {}
133 + existingConfig.gossip[name] = value
134 +
135 + // write to disc
136 + fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
137 + }
138 +
139 + var gossip = {
140 + wakeup: 0,
141 + peers: function () {
142 + return peers
143 + },
144 + get: function (addr) {
145 + addr = ref.parseAddress(addr)
146 + return u.find(peers, function (a) {
147 + return (
148 + addr.port === a.port
149 + && addr.host === a.host
150 + && addr.key === a.key
151 + )
152 + })
153 + },
154 + connect: valid.async(function (addr, cb) {
155 + console.log("CONNECT", addr)
156 + addr = ref.parseAddress(addr)
157 + if (!addr || typeof addr != 'object')
158 + return cb(new Error('first param must be an address'))
159 +
160 + if(!addr.key) return cb(new Error('address must have ed25519 key'))
161 + // add peer to the table, incase it isn't already.
162 + gossip.add(addr, 'manual')
163 + var p = gossip.get(addr)
164 + if(!p) return cb()
165 +
166 + p.stateChange = Date.now()
167 + p.state = 'connecting'
168 + server.connect(p, function (err, rpc) {
169 + if (err) {
170 + p.error = err.stack
171 + p.state = undefined
172 + p.failure = (p.failure || 0) + 1
173 + p.stateChange = Date.now()
174 + notify({ type: 'connect-failure', peer: p })
175 + server.emit('log:info', ['SBOT', p.host+':'+p.port+p.key, 'connection failed', err.message || err])
176 + p.duration = stats(p.duration, 0)
177 + return (cb && cb(err))
178 + }
179 + else {
180 + delete p.error
181 + p.state = 'connected'
182 + p.failure = 0
183 + }
184 + cb && cb(null, rpc)
185 + })
186 +
187 + }, 'string|object'),
188 +
189 + disconnect: valid.async(function (addr, cb) {
190 + var peer = this.get(addr)
191 +
192 + peer.state = 'disconnecting'
193 + peer.stateChange = Date.now()
194 + if(!peer || !peer.disconnect) cb && cb()
195 + else peer.disconnect(true, function (err) {
196 + peer.stateChange = Date.now()
197 + })
198 +
199 + }, 'string|object'),
200 +
201 + changes: function () {
202 + return notify.listen()
203 + },
204 + //add an address to the peer table.
205 + add: valid.sync(function (addr, source) {
206 + addr = ref.parseAddress(addr)
207 + if(!ref.isAddress(addr))
208 + throw new Error('not a valid address:' + JSON.stringify(addr))
209 + // check that this is a valid address, and not pointing at self.
210 +
211 + if(addr.key === home.key) return
212 + if(addr.host === home.host && addr.port === home.port) return
213 +
214 + var f = gossip.get(addr)
215 +
216 + if(!f) {
217 + // new peer
218 + addr.source = source
219 + addr.announcers = 1
220 + addr.duration = addr.duration || null
221 + peers.push(addr)
222 + notify({ type: 'discover', peer: addr, source: source || 'manual' })
223 + return addr
224 + } else if (source === 'local') {
225 + // this peer is local, override old source to prioritize gossip
226 + f.source = source
227 + }
228 + //don't count local over and over
229 + else if(f.source != 'local')
230 + f.announcers ++
231 +
232 + return f
233 + }, 'string|object', 'string?'),
234 + remove: function (addr) {
235 + var peer = gossip.get(addr)
236 + var index = peers.indexOf(peer)
237 + if (~index) {
238 + peers.splice(index, 1)
239 + notify({ type: 'remove', peer: peer })
240 + }
241 + },
242 + //ping: function (opts) {
243 + // var timeout = config.timers && config.timers.ping || 5*60e3
244 + //between 10 seconds and 30 minutes, default 5 min
245 + // timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
246 + // return ping({timeout: timeout})
247 + //},
248 + reconnect: function () {
249 + for(var id in server.peers)
250 + if(id !== server.id) //don't disconnect local client
251 + server.peers[id].forEach(function (peer) {
252 + peer.close(true)
253 + })
254 + return gossip.wakeup = Date.now()
255 + },
256 + enable: valid.sync(function (type) {
257 + type = type || 'global'
258 + setConfig(type, true)
259 + if(type === 'local' && server.local && server.local.init)
260 + server.local.init()
261 + return 'enabled gossip type ' + type
262 + }, 'string?'),
263 + disable: valid.sync(function (type) {
264 + type = type || 'global'
265 + setConfig(type, false)
266 + return 'disabled gossip type ' + type
267 + }, 'string?')
268 + }
269 +
270 + closeScheduler = Schedule (gossip, config, server)
271 + Init (gossip, config, server)
272 + //get current state
273 +
274 + server.on('rpc:connect', function (rpc, isClient) {
275 +
276 + // if we're not ready, close this connection immediately
277 + if (!server.ready() && rpc.id !== server.id) return rpc.close()
278 +
279 + var peer = getPeer(rpc.id)
280 + //don't track clients that connect, but arn't considered peers.
281 + //maybe we should though?
282 + if(!peer) {
283 + if(rpc.id !== server.id) {
284 + console.log('Connected', rpc.id)
285 + rpc.on('closed', function () {
286 + console.log('Disconnected', rpc.id)
287 + })
288 + }
289 + return
290 + }
291 +
292 + status[rpc.id] = simplify(peer)
293 +
294 + console.log('Connected', stringify(peer))
295 + //means that we have created this connection, not received it.
296 + peer.client = !!isClient
297 + peer.state = 'connected'
298 + peer.stateChange = Date.now()
299 + peer.disconnect = function (err, cb) {
300 + if(isFunction(err)) cb = err, err = null
301 + rpc.close(err, cb)
302 + }
303 +
304 + //if(isClient) {
305 + //default ping is 5 minutes...
306 + //var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
307 + //peer.ping = {rtt: pp.rtt, skew: pp.skew}
308 + //pull(
309 + //pp,
310 + //rpc.gossip.ping({timeout: timer_ping}, function (err) {
311 + // if(err.name === 'TypeError') peer.ping.fail = true
312 + //}),
313 + //pp
314 + //)
315 + //}
316 +
317 + rpc.on('closed', function () {
318 + delete status[rpc.id]
319 + console.log('Disconnected', stringify(peer))
320 + //track whether we have successfully connected.
321 + //or how many failures there have been.
322 + var since = peer.stateChange
323 + peer.stateChange = Date.now()
324 +// if(peer.state === 'connected') //may be "disconnecting"
325 + peer.duration = stats(peer.duration, peer.stateChange - since)
326 + peer.state = undefined
327 + notify({ type: 'disconnect', peer: peer })
328 + server.emit('log:info', ['SBOT', rpc.id, 'disconnect'])
329 + })
330 +
331 + notify({ type: 'connect', peer: peer })
332 + })
333 +
334 + var last
335 + stateFile.get(function (err, ary) {
336 + last = ary || []
337 + if(Array.isArray(ary))
338 + ary.forEach(function (v) {
339 + delete v.state
340 + // don't add local peers (wait to rediscover)
341 + if(v.source !== 'local') {
342 + gossip.add(v, 'stored')
343 + }
344 + })
345 + })
346 +
347 + var int = setInterval(function () {
348 + var copy = JSON.parse(JSON.stringify(peers))
349 + copy.filter(function (e) {
350 + return e.source !== 'local'
351 + }).forEach(function (e) {
352 + delete e.state
353 + })
354 + if(deepEqual(copy, last)) return
355 + last = copy
356 + stateFile.set(copy, function(err) {
357 + if (err) console.log(err)
358 + })
359 + }, 10*1000)
360 +
361 + if(int.unref) int.unref()
362 +
363 + return gossip
364 + }
365 +}
366 +
367 +
plugins/hackedgossip/init.jsView
@@ -1,0 +1,32 @@
1 +var isArray = Array.isArray
2 +var pull = require('pull-stream')
3 +var ref = require('ssb-ref')
4 +
5 +module.exports = function (gossip, config, server) {
6 + if (config.offline) return void console.log("Running in offline mode: gossip disabled")
7 +
8 + // populate peertable with configured seeds (mainly used in testing)
9 + var seeds = config.seeds
10 +
11 + ;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
12 + .forEach(function (addr) { gossip.add(addr, 'seed') })
13 +
14 + // populate peertable with pub announcements on the feed
15 + pull(
16 + server.messagesByType({
17 + type: 'pub', live: true, keys: false
18 + }),
19 + pull.drain(function (msg) {
20 + if(msg.sync) return
21 + if(!msg.content.address) return
22 + if(ref.isAddress(msg.content.address))
23 + gossip.add(msg.content.address, 'pub')
24 + })
25 + )
26 +
27 + // populate peertable with announcements on the LAN multicast
28 + server.on('local', function (_peer) {
29 + gossip.add(_peer, 'local')
30 + })
31 +
32 +}
plugins/hackedgossip/schedule.jsView
@@ -1,0 +1,253 @@
1 +'use strict'
2 +var ip = require('ip')
3 +var onWakeup = require('on-wakeup')
4 +var onNetwork = require('on-change-network')
5 +var hasNetwork = require('../../lib/has-network-debounced')
6 +
7 +var pull = require('pull-stream')
8 +
9 +function not (fn) {
10 + return function (e) { return !fn(e) }
11 +}
12 +
13 +function and () {
14 + var args = [].slice.call(arguments)
15 + return function (value) {
16 + return args.every(function (fn) { return fn.call(null, value) })
17 + }
18 +}
19 +
20 +//min delay (delay since last disconnect of most recent peer in unconnected set)
21 +//unconnected filter delay peer < min delay
22 +function delay (failures, factor, max) {
23 + return Math.min(Math.pow(2, failures)*factor, max || Infinity)
24 +}
25 +
26 +function maxStateChange (M, e) {
27 + return Math.max(M, e.stateChange || 0)
28 +}
29 +
30 +function peerNext(peer, opts) {
31 + return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
32 +}
33 +
34 +
35 +//detect if not connected to wifi or other network
36 +//(i.e. if there is only localhost)
37 +
38 +function isOffline (e) {
39 + if(ip.isLoopback(e.host) || e.host == 'localhost') return false
40 + return !hasNetwork()
41 +}
42 +
43 +var isOnline = not(isOffline)
44 +
45 +function isLocal (e) {
46 + // don't rely on private ip address, because
47 + // cjdns creates fake private ip addresses.
48 + // ignore localhost addresses, because sometimes they get broadcast.
49 + return !ip.isLoopback(e.host) && ip.isPrivate(e.host) && e.source === 'local'
50 +}
51 +
52 +function isSeed (e) {
53 + return e.source === 'seed'
54 +}
55 +
56 +//function isFriend (e) {
57 +// return e.source === 'friends'
58 +//}
59 +
60 +function isUnattempted (e) {
61 + return !e.stateChange
62 +}
63 +
64 +//select peers which have never been successfully connected to yet,
65 +//but have been tried.
66 +function isInactive (e) {
67 + return e.stateChange && (!e.duration || e.duration.mean == 0)
68 +}
69 +
70 +function isLongterm (e) {
71 + return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
72 +}
73 +
74 +//peers which we can connect to, but are not upgraded.
75 +//select peers which we can connect to, but are not upgraded to LT.
76 +//assume any peer is legacy, until we know otherwise...
77 +function isLegacy (peer) {
78 + return peer.duration && (peer.duration && peer.duration.mean > 0) && !exports.isLongterm(peer)
79 +}
80 +
81 +function isConnect (e) {
82 + return 'connected' === e.state || 'connecting' === e.state
83 +}
84 +
85 +//sort oldest to newest then take first n
86 +function earliest(peers, n) {
87 + return peers.sort(function (a, b) {
88 + return a.stateChange - b.stateChange
89 + }).slice(0, Math.max(n, 0))
90 +}
91 +
92 +function select(peers, ts, filter, opts) {
93 + if(opts.disable) return []
94 + //opts: { quota, groupMin, min, factor, max }
95 + var type = peers.filter(filter)
96 + var unconnect = type.filter(not(isConnect))
97 + var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
98 + var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
99 + if(ts < min) return []
100 +
101 + return earliest(unconnect.filter(function (peer) {
102 + return peerNext(peer, opts) < ts
103 + }), count)
104 +}
105 +
106 +var schedule = exports = module.exports =
107 +function (gossip, config, server) {
108 + var min = 60e3, hour = 60*60e3, closed = false
109 +
110 + //trigger hard reconnect after suspend or local network changes
111 + onWakeup(gossip.reconnect)
112 + onNetwork(gossip.reconnect)
113 +
114 + function conf(name, def) {
115 + if(config.gossip == null) return def
116 + var value = config.gossip[name]
117 + return (value == null || value === '') ? def : value
118 + }
119 +
120 + function connect (peers, ts, name, filter, opts) {
121 + opts.group = name
122 + var connected = peers.filter(isConnect).filter(filter)
123 +
124 + //disconnect if over quota
125 + if(connected.length > opts.quota) {
126 + return earliest(connected, connected.length - opts.quota)
127 + .forEach(function (peer) {
128 + gossip.disconnect(peer)
129 + })
130 + }
131 +
132 + //will return [] if the quota is full
133 + var selected = select(peers, ts, and(filter, isOnline), opts)
134 + selected
135 + .forEach(function (peer) {
136 + gossip.connect(peer)
137 + })
138 + }
139 +
140 +
141 + var connecting = false
142 + function connections () {
143 + if(connecting || closed) return
144 + connecting = true
145 + var timer = setTimeout(function () {
146 + connecting = false
147 +
148 + // don't attempt to connect while migration is running
149 + if (!server.ready()) return
150 +
151 + var ts = Date.now()
152 + var peers = gossip.peers()
153 +
154 + var connected = peers.filter(and(isConnect, not(isLocal))).length
155 +
156 + var connectedFriends = peers.filter(and(isConnect)).length
157 +
158 + if(conf('seed', true))
159 + connect(peers, ts, 'seeds', isSeed, {
160 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
161 + })
162 +
163 + if(conf('local', true))
164 + connect(peers, ts, 'local', isLocal, {
165 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
166 + })
167 +
168 + if(conf('global', true)) {
169 + /* // prioritize friends
170 + connect(peers, ts, 'friends', and(exports.isFriend, exports.isLongterm), {
171 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
172 + })
173 +
174 + if (connectedFriends < 2)
175 + connect(peers, ts, 'attemptFriend', and(exports.isFriend, exports.isUnattempted), {
176 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
177 + })
178 +
179 + connect(peers, ts, 'retryFriends', and(exports.isFriend, exports.isInactive), {
180 + min: 0,
181 + quota: 3, factor: 60e3, max: 3*60*60e3, groupMin: 5*60e3,
182 + })
183 + */
184 +
185 + // standard longterm peers
186 + connect(peers, ts, 'longterm', and(
187 + exports.isLongterm,
188 + //not(exports.isFriend),
189 + not(exports.isLocal)
190 + ), {
191 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
192 + })
193 +
194 + if(!connected)
195 + connect(peers, ts, 'attempt', exports.isUnattempted, {
196 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
197 + })
198 +
199 + //quota, groupMin, min, factor, max
200 + connect(peers, ts, 'retry', exports.isInactive, {
201 + min: 0,
202 + quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3,
203 + })
204 +
205 + var longterm = peers.filter(isConnect).filter(isLongterm).length
206 +
207 + connect(peers, ts, 'legacy', exports.isLegacy, {
208 + quota: 3 - longterm,
209 + factor: 5*min, max: 3*hour, groupMin: 5*min,
210 + })
211 + }
212 +
213 + peers.filter(isConnect).forEach(function (e) {
214 + var permanent = exports.isLongterm(e) || exports.isLocal(e)
215 + if((!permanent || e.state === 'connecting') && e.stateChange + 10e3 < ts) {
216 + gossip.disconnect(e)
217 + }
218 + })
219 +
220 + }, 100*Math.random())
221 + if(timer.unref) timer.unref()
222 + }
223 +
224 + pull(
225 + gossip.changes(),
226 + pull.drain(function (ev) {
227 + if(ev.type == 'disconnect')
228 + connections()
229 + })
230 + )
231 +
232 + var int = setInterval(connections, 0)
233 +
234 + connections()
235 +
236 + return function onClose () {
237 + closed = true
238 + }
239 +
240 +}
241 +
242 +exports.isUnattempted = isUnattempted
243 +exports.isInactive = isInactive
244 +exports.isLongterm = isLongterm
245 +exports.isLegacy = isLegacy
246 +exports.isLocal = isLocal
247 +//exports.isFriend = isFriend
248 +exports.isConnectedOrConnecting = isConnect
249 +exports.select = select
250 +
251 +
252 +
253 +
plugins/moderngossip/index.jsView
@@ -1,0 +1,372 @@
1 +'use strict'
2 +var pull = require('pull-stream')
3 +var Notify = require('pull-notify')
4 +var mdm = require('mdmanifest')
5 +var valid = require('../../lib/validators')
6 +var apidoc = require('../../lib/apidocs').gossip
7 +var u = require('../../lib/util')
8 +var ref = require('ssb-ref')
9 +var ping = require('pull-ping')
10 +var stats = require('statistics')
11 +var Schedule = require('./schedule')
12 +var Init = require('./init')
13 +var AtomicFile = require('atomic-file')
14 +var fs = require('fs')
15 +var path = require('path')
16 +var deepEqual = require('deep-equal')
17 +
18 +function isFunction (f) {
19 + return 'function' === typeof f
20 +}
21 +
22 +function stringify(peer) {
23 + return [peer.host, peer.port, peer.key].join(':')
24 +}
25 +
26 +function isObject (o) {
27 + return o && 'object' == typeof o
28 +}
29 +
30 +function toBase64 (s) {
31 + if(isString(s)) return s
32 + else s.toString('base64') //assume a buffer
33 +}
34 +
35 +function isString (s) {
36 + return 'string' == typeof s
37 +}
38 +
39 +function coearseAddress (address) {
40 + if(isObject(address)) {
41 + var protocol = 'net'
42 + if (address.host.endsWith(".onion"))
43 + protocol = 'onion'
44 + return [protocol, address.host, address.port].join(':') +'~'+['shs', toBase64(address.key)].join(':')
45 + }
46 + return address
47 +}
48 +
49 +/*
50 +Peers : [{
51 + key: id,
52 + host: ip,
53 + port: int,
54 + //to be backwards compatible with patchwork...
55 + announcers: {length: int}
56 + source: 'pub'|'manual'|'local'
57 +}]
58 +*/
59 +
60 +
61 +module.exports = {
62 + name: 'gossip',
63 + version: '1.0.0',
64 + manifest: mdm.manifest(apidoc),
65 + permissions: {
66 + anonymous: {allow: ['ping']}
67 + },
68 + init: function (server, config) {
69 + var notify = Notify()
70 + var closed = false, closeScheduler
71 + var conf = config.gossip || {}
72 + var home = ref.parseAddress(server.getAddress())
73 +
74 + var stateFile = AtomicFile(path.join(config.path, 'gossip.json'))
75 +
76 + var status = {}
77 +
78 + //Known Peers
79 + var peers = []
80 +
81 + function getPeer(id) {
82 + return u.find(peers, function (e) {
83 + return e && e.key === id
84 + })
85 + }
86 +
87 + function simplify (peer) {
88 + return {
89 + address: coearseAddress(peer),
90 + source: peer.source,
91 + state: peer.state, stateChange: peer.stateChange,
92 + failure: peer.failure,
93 + client: peer.client,
94 + stats: {
95 + duration: peer.duration || undefined,
96 + rtt: peer.ping ? peer.ping.rtt : undefined,
97 + skew: peer.ping ? peer.ping.skew : undefined,
98 + }
99 + }
100 + }
101 +
102 + server.status.hook(function (fn) {
103 + var _status = fn()
104 + _status.gossip = status
105 + peers.forEach(function (peer) {
106 + if(peer.stateChange + 3e3 > Date.now() || peer.state === 'connected')
107 + status[peer.key] = simplify(peer)
108 + })
109 + return _status
110 +
111 + })
112 +
113 + server.close.hook(function (fn, args) {
114 + closed = true
115 + closeScheduler()
116 + for(var id in server.peers)
117 + server.peers[id].forEach(function (peer) {
118 + peer.close(true)
119 + })
120 + return fn.apply(this, args)
121 + })
122 +
123 + var timer_ping = 5*6e4
124 +
125 + function setConfig(name, value) {
126 + config.gossip = config.gossip || {}
127 + config.gossip[name] = value
128 +
129 + var cfgPath = path.join(config.path, 'config')
130 + var existingConfig = {}
131 +
132 + // load ~/.ssb/config
133 + try { existingConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) }
134 + catch (e) {}
135 +
136 + // update the plugins config
137 + existingConfig.gossip = existingConfig.gossip || {}
138 + existingConfig.gossip[name] = value
139 +
140 + // write to disc
141 + fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
142 + }
143 +
144 + var gossip = {
145 + wakeup: 0,
146 + peers: function () {
147 + return peers
148 + },
149 + get: function (addr) {
150 + addr = ref.parseAddress(addr)
151 + return u.find(peers, function (a) {
152 + return (
153 + addr.port === a.port
154 + && addr.host === a.host
155 + && addr.key === a.key
156 + )
157 + })
158 + },
159 + connect: valid.async(function (addr, cb) {
160 + console.log("CONNECT", addr)
161 + addr = ref.parseAddress(addr)
162 + if (!addr || typeof addr != 'object')
163 + return cb(new Error('first param must be an address'))
164 +
165 + if(!addr.key) return cb(new Error('address must have ed25519 key'))
166 + // add peer to the table, incase it isn't already.
167 + gossip.add(addr, 'manual')
168 + var p = gossip.get(addr)
169 + if(!p) return cb()
170 +
171 + p.stateChange = Date.now()
172 + p.state = 'connecting'
173 + server.connect(coearseAddress(p), function (err, rpc) {
174 + if (err) {
175 + p.error = err.stack
176 + p.state = undefined
177 + p.failure = (p.failure || 0) + 1
178 + p.stateChange = Date.now()
179 + notify({ type: 'connect-failure', peer: p })
180 + server.emit('log:info', ['SBOT', p.host+':'+p.port+p.key, 'connection failed', err.message || err])
181 + p.duration = stats(p.duration, 0)
182 + return (cb && cb(err))
183 + }
184 + else {
185 + delete p.error
186 + p.state = 'connected'
187 + p.failure = 0
188 + }
189 + cb && cb(null, rpc)
190 + })
191 +
192 + }, 'string|object'),
193 +
194 + disconnect: valid.async(function (addr, cb) {
195 + var peer = this.get(addr)
196 +
197 + peer.state = 'disconnecting'
198 + peer.stateChange = Date.now()
199 + if(!peer || !peer.disconnect) cb && cb()
200 + else peer.disconnect(true, function (err) {
201 + peer.stateChange = Date.now()
202 + })
203 +
204 + }, 'string|object'),
205 +
206 + changes: function () {
207 + return notify.listen()
208 + },
209 + //add an address to the peer table.
210 + add: valid.sync(function (addr, source) {
211 + addr = ref.parseAddress(addr)
212 + if(!ref.isAddress(addr))
213 + throw new Error('not a valid address:' + JSON.stringify(addr))
214 + // check that this is a valid address, and not pointing at self.
215 +
216 + if(addr.key === home.key) return
217 + if(addr.host === home.host && addr.port === home.port) return
218 +
219 + var f = gossip.get(addr)
220 +
221 + if(!f) {
222 + // new peer
223 + addr.source = source
224 + addr.announcers = 1
225 + addr.duration = addr.duration || null
226 + peers.push(addr)
227 + notify({ type: 'discover', peer: addr, source: source || 'manual' })
228 + return addr
229 + } else if (source === 'friends' || source === 'local') {
230 + // this peer is a friend or local, override old source to prioritize gossip
231 + f.source = source
232 + }
233 + //don't count local over and over
234 + else if(f.source != 'local')
235 + f.announcers ++
236 +
237 + return f
238 + }, 'string|object', 'string?'),
239 + remove: function (addr) {
240 + var peer = gossip.get(addr)
241 + var index = peers.indexOf(peer)
242 + if (~index) {
243 + peers.splice(index, 1)
244 + notify({ type: 'remove', peer: peer })
245 + }
246 + },
247 + ping: function (opts) {
248 + var timeout = config.timers && config.timers.ping || 5*60e3
249 + //between 10 seconds and 30 minutes, default 5 min
250 + timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
251 + return ping({timeout: timeout})
252 + },
253 + reconnect: function () {
254 + for(var id in server.peers)
255 + if(id !== server.id) //don't disconnect local client
256 + server.peers[id].forEach(function (peer) {
257 + peer.close(true)
258 + })
259 + return gossip.wakeup = Date.now()
260 + },
261 + enable: valid.sync(function (type) {
262 + type = type || 'global'
263 + setConfig(type, true)
264 + if(type === 'local' && server.local && server.local.init)
265 + server.local.init()
266 + return 'enabled gossip type ' + type
267 + }, 'string?'),
268 + disable: valid.sync(function (type) {
269 + type = type || 'global'
270 + setConfig(type, false)
271 + return 'disabled gossip type ' + type
272 + }, 'string?')
273 + }
274 +
275 + closeScheduler = Schedule (gossip, config, server)
276 + Init (gossip, config, server)
277 + //get current state
278 +
279 + server.on('rpc:connect', function (rpc, isClient) {
280 +
281 + // if we're not ready, close this connection immediately
282 + if (!server.ready() && rpc.id !== server.id) return rpc.close()
283 +
284 + var peer = getPeer(rpc.id)
285 + //don't track clients that connect, but arn't considered peers.
286 + //maybe we should though?
287 + if(!peer) {
288 + if(rpc.id !== server.id) {
289 + console.log('Connected', rpc.id)
290 + rpc.on('closed', function () {
291 + console.log('Disconnected', rpc.id)
292 + })
293 + }
294 + return
295 + }
296 +
297 + status[rpc.id] = simplify(peer)
298 +
299 + console.log('Connected', stringify(peer))
300 + //means that we have created this connection, not received it.
301 + peer.client = !!isClient
302 + peer.state = 'connected'
303 + peer.stateChange = Date.now()
304 + peer.disconnect = function (err, cb) {
305 + if(isFunction(err)) cb = err, err = null
306 + rpc.close(err, cb)
307 + }
308 +
309 + if(isClient) {
310 + //default ping is 5 minutes...
311 + var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
312 + peer.ping = {rtt: pp.rtt, skew: pp.skew}
313 + pull(
314 + pp,
315 + rpc.gossip.ping({timeout: timer_ping}, function (err) {
316 + if(err.name === 'TypeError') peer.ping.fail = true
317 + }),
318 + pp
319 + )
320 + }
321 +
322 + rpc.on('closed', function () {
323 + delete status[rpc.id]
324 + console.log('Disconnected', stringify(peer))
325 + //track whether we have successfully connected.
326 + //or how many failures there have been.
327 + var since = peer.stateChange
328 + peer.stateChange = Date.now()
329 +// if(peer.state === 'connected') //may be "disconnecting"
330 + peer.duration = stats(peer.duration, peer.stateChange - since)
331 + peer.state = undefined
332 + notify({ type: 'disconnect', peer: peer })
333 + server.emit('log:info', ['SBOT', rpc.id, 'disconnect'])
334 + })
335 +
336 + notify({ type: 'connect', peer: peer })
337 + })
338 +
339 + var last
340 + stateFile.get(function (err, ary) {
341 + last = ary || []
342 + if(Array.isArray(ary))
343 + ary.forEach(function (v) {
344 + delete v.state
345 + // don't add local peers (wait to rediscover)
346 + if(v.source !== 'local') {
347 + gossip.add(v, 'stored')
348 + }
349 + })
350 + })
351 +
352 + var int = setInterval(function () {
353 + var copy = JSON.parse(JSON.stringify(peers))
354 + copy.filter(function (e) {
355 + return e.source !== 'local'
356 + }).forEach(function (e) {
357 + delete e.state
358 + })
359 + if(deepEqual(copy, last)) return
360 + last = copy
361 + stateFile.set(copy, function(err) {
362 + if (err) console.log(err)
363 + })
364 + }, 10*1000)
365 +
366 + if(int.unref) int.unref()
367 +
368 + return gossip
369 + }
370 +}
371 +
372 +
plugins/moderngossip/init.jsView
@@ -1,0 +1,32 @@
1 +var isArray = Array.isArray
2 +var pull = require('pull-stream')
3 +var ref = require('ssb-ref')
4 +
5 +module.exports = function (gossip, config, server) {
6 + if (config.offline) return void console.log("Running in offline mode: gossip disabled")
7 +
8 + // populate peertable with configured seeds (mainly used in testing)
9 + var seeds = config.seeds
10 +
11 + ;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
12 + .forEach(function (addr) { gossip.add(addr, 'seed') })
13 +
14 + // populate peertable with pub announcements on the feed
15 + pull(
16 + server.messagesByType({
17 + type: 'pub', live: true, keys: false
18 + }),
19 + pull.drain(function (msg) {
20 + if(msg.sync) return
21 + if(!msg.content.address) return
22 + if(ref.isAddress(msg.content.address))
23 + gossip.add(msg.content.address, 'pub')
24 + })
25 + )
26 +
27 + // populate peertable with announcements on the LAN multicast
28 + server.on('local', function (_peer) {
29 + gossip.add(_peer, 'local')
30 + })
31 +
32 +}
plugins/moderngossip/schedule.jsView
@@ -1,0 +1,253 @@
1 +'use strict'
2 +var ip = require('ip')
3 +var onWakeup = require('on-wakeup')
4 +var onNetwork = require('on-change-network')
5 +var hasNetwork = require('../../lib/has-network-debounced')
6 +
7 +var pull = require('pull-stream')
8 +
9 +function not (fn) {
10 + return function (e) { return !fn(e) }
11 +}
12 +
13 +function and () {
14 + var args = [].slice.call(arguments)
15 + return function (value) {
16 + return args.every(function (fn) { return fn.call(null, value) })
17 + }
18 +}
19 +
20 +//min delay (delay since last disconnect of most recent peer in unconnected set)
21 +//unconnected filter delay peer < min delay
22 +function delay (failures, factor, max) {
23 + return Math.min(Math.pow(2, failures)*factor, max || Infinity)
24 +}
25 +
26 +function maxStateChange (M, e) {
27 + return Math.max(M, e.stateChange || 0)
28 +}
29 +
30 +function peerNext(peer, opts) {
31 + return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
32 +}
33 +
34 +
35 +//detect if not connected to wifi or other network
36 +//(i.e. if there is only localhost)
37 +
38 +function isOffline (e) {
39 + if(ip.isLoopback(e.host) || e.host == 'localhost') return false
40 + return !hasNetwork()
41 +}
42 +
43 +var isOnline = not(isOffline)
44 +
45 +function isLocal (e) {
46 + // don't rely on private ip address, because
47 + // cjdns creates fake private ip addresses.
48 + // ignore localhost addresses, because sometimes they get broadcast.
49 + return !ip.isLoopback(e.host) && ip.isPrivate(e.host) && e.source === 'local'
50 +}
51 +
52 +function isSeed (e) {
53 + return e.source === 'seed'
54 +}
55 +
56 +function isFriend (e) {
57 + return e.source === 'friends'
58 +}
59 +
60 +function isUnattempted (e) {
61 + return !e.stateChange
62 +}
63 +
64 +//select peers which have never been successfully connected to yet,
65 +//but have been tried.
66 +function isInactive (e) {
67 + return e.stateChange && (!e.duration || e.duration.mean == 0)
68 +}
69 +
70 +function isLongterm (e) {
71 + return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
72 +}
73 +
74 +//peers which we can connect to, but are not upgraded.
75 +//select peers which we can connect to, but are not upgraded to LT.
76 +//assume any peer is legacy, until we know otherwise...
77 +function isLegacy (peer) {
78 + return peer.duration && (peer.duration && peer.duration.mean > 0) && !exports.isLongterm(peer)
79 +}
80 +
81 +function isConnect (e) {
82 + return 'connected' === e.state || 'connecting' === e.state
83 +}
84 +
85 +//sort oldest to newest then take first n
86 +function earliest(peers, n) {
87 + return peers.sort(function (a, b) {
88 + return a.stateChange - b.stateChange
89 + }).slice(0, Math.max(n, 0))
90 +}
91 +
92 +function select(peers, ts, filter, opts) {
93 + if(opts.disable) return []
94 + //opts: { quota, groupMin, min, factor, max }
95 + var type = peers.filter(filter)
96 + var unconnect = type.filter(not(isConnect))
97 + var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
98 + var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
99 + if(ts < min) return []
100 +
101 + return earliest(unconnect.filter(function (peer) {
102 + return peerNext(peer, opts) < ts
103 + }), count)
104 +}
105 +
106 +var schedule = exports = module.exports =
107 +function (gossip, config, server) {
108 + var min = 60e3, hour = 60*60e3, closed = false
109 +
110 + //trigger hard reconnect after suspend or local network changes
111 + onWakeup(gossip.reconnect)
112 + onNetwork(gossip.reconnect)
113 +
114 + function conf(name, def) {
115 + if(config.gossip == null) return def
116 + var value = config.gossip[name]
117 + return (value == null || value === '') ? def : value
118 + }
119 +
120 + function connect (peers, ts, name, filter, opts) {
121 + opts.group = name
122 + var connected = peers.filter(isConnect).filter(filter)
123 +
124 + //disconnect if over quota
125 + if(connected.length > opts.quota) {
126 + return earliest(connected, connected.length - opts.quota)
127 + .forEach(function (peer) {
128 + gossip.disconnect(peer)
129 + })
130 + }
131 +
132 + //will return [] if the quota is full
133 + var selected = select(peers, ts, and(filter, isOnline), opts)
134 + selected
135 + .forEach(function (peer) {
136 + gossip.connect(peer)
137 + })
138 + }
139 +
140 +
141 + var connecting = false
142 + function connections () {
143 + if(connecting || closed) return
144 + connecting = true
145 + var timer = setTimeout(function () {
146 + connecting = false
147 +
148 + // don't attempt to connect while migration is running
149 + if (!server.ready()) return
150 +
151 + var ts = Date.now()
152 + var peers = gossip.peers()
153 +
154 + var connected = peers.filter(and(isConnect, not(isLocal), not(isFriend))).length
155 +
156 + var connectedFriends = peers.filter(and(isConnect, isFriend)).length
157 +
158 + if(conf('seed', true))
159 + connect(peers, ts, 'seeds', isSeed, {
160 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
161 + })
162 +
163 + if(conf('local', true))
164 + connect(peers, ts, 'local', isLocal, {
165 + quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
166 + })
167 +
168 + if(conf('global', true)) {
169 + // prioritize friends
170 + connect(peers, ts, 'friends', and(exports.isFriend, exports.isLongterm), {
171 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
172 + })
173 +
174 + if (connectedFriends < 2)
175 + connect(peers, ts, 'attemptFriend', and(exports.isFriend, exports.isUnattempted), {
176 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
177 + })
178 +
179 + connect(peers, ts, 'retryFriends', and(exports.isFriend, exports.isInactive), {
180 + min: 0,
181 + quota: 3, factor: 60e3, max: 3*60*60e3, groupMin: 5*60e3,
182 + })
183 +
184 + // standard longterm peers
185 + connect(peers, ts, 'longterm', and(
186 + exports.isLongterm,
187 + not(exports.isFriend),
188 + not(exports.isLocal)
189 + ), {
190 + quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
191 + })
192 +
193 + if(!connected)
194 + connect(peers, ts, 'attempt', exports.isUnattempted, {
195 + min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
196 + })
197 +
198 + //quota, groupMin, min, factor, max
199 + connect(peers, ts, 'retry', exports.isInactive, {
200 + min: 0,
201 + quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3,
202 + })
203 +
204 + var longterm = peers.filter(isConnect).filter(isLongterm).length
205 +
206 + connect(peers, ts, 'legacy', exports.isLegacy, {
207 + quota: 3 - longterm,
208 + factor: 5*min, max: 3*hour, groupMin: 5*min,
209 + })
210 + }
211 +
212 + peers.filter(isConnect).forEach(function (e) {
213 + var permanent = exports.isLongterm(e) || exports.isLocal(e)
214 + if((!permanent || e.state === 'connecting') && e.stateChange + 10e3 < ts) {
215 + gossip.disconnect(e)
216 + }
217 + })
218 +
219 + }, 100*Math.random())
220 + if(timer.unref) timer.unref()
221 + }
222 +
223 + pull(
224 + gossip.changes(),
225 + pull.drain(function (ev) {
226 + if(ev.type == 'disconnect')
227 + connections()
228 + })
229 + )
230 +
231 + var int = setInterval(connections, 2e3)
232 + if(int.unref) int.unref()
233 +
234 + connections()
235 +
236 + return function onClose () {
237 + closed = true
238 + }
239 +
240 +}
241 +
242 +exports.isUnattempted = isUnattempted
243 +exports.isInactive = isInactive
244 +exports.isLongterm = isLongterm
245 +exports.isLegacy = isLegacy
246 +exports.isLocal = isLocal
247 +exports.isFriend = isFriend
248 +exports.isConnectedOrConnecting = isConnect
249 +exports.select = select
250 +
251 +
252 +
253 +

Built with git-ssb-web