Commit cc82650a85e0b733ac8fe1c96af74e69231faf4c
three different gossip algos
Ev Bogue committed on 9/10/2017, 12:07:58 AMParent: 9729cbbd473487282f0ef7b486cb87adb8c622f8
Files changed
decent.js | ||
---|---|---|
@@ -11,9 +11,9 @@ | ||
11 | 11 … | var manifestFile = path.join(config.path, 'manifest.json') |
12 | 12 … | |
13 | 13 … | var createSbot = require('./lib') |
14 | 14 … | .use(require('./plugins/master')) |
15 | - .use(require('./plugins/gossip')) | |
15 … | + .use(require('./plugins/moderngossip')) | |
16 | 16 … | .use(require('./plugins/replicate')) |
17 | 17 … | .use(require('ssb-friends')) |
18 | 18 … | .use(require('ssb-blobs')) |
19 | 19 … | .use(require('./plugins/local')) |
plugins/ssb-config/inject.js | ||
---|---|---|
@@ -9,11 +9,12 @@ | ||
9 | 9 … | var SEC = 1e3 |
10 | 10 … | var MIN = 60*SEC |
11 | 11 … | |
12 | 12 … | module.exports = function (name, override) { |
13 … | + //name = name || 'ssb' | |
13 | 14 … | name = name || 'decent' |
14 | 15 … | var HOME = home() || 'browser' //most probably browser |
15 | - return RC(name || 'decent', merge({ | |
16 … | + return RC(name, merge({ | |
16 | 17 … | //just use an ipv4 address by default. |
17 | 18 … | //there have been some reports of seemingly non-private |
18 | 19 … | //ipv6 addresses being returned and not working. |
19 | 20 … | //https://github.com/ssbc/scuttlebot/pull/102 |
@@ -45,9 +46,9 @@ | ||
45 | 46 … | //this will be updated whenever breaking changes are made. |
46 | 47 … | //(see secret-handshake paper for a full explaination) |
47 | 48 … | //(generated by crypto.randomBytes(32).toString('base64')) |
48 | 49 … | shs: 'EVRctE2Iv8GrO/BpQCF34e2FMPsDJot9x0j846LjVtc=', |
49 | - | |
50 … | + //shs: '1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=', | |
50 | 51 … | //used to sign messages |
51 | 52 … | sign: null |
52 | 53 … | }, |
53 | 54 … | master: [], |
plugins/classicgossip/index.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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