+2
-5
bin.js
+2
-5
bin.js
···
50
50
.use(require('ssb-master'))
51
51
.use(require('ssb-gossip'))
52
52
.use(require('ssb-replicate'))
53
-
.use(require('ssb-friends'))
53
+
.use(require('./plugins/friends'))
54
54
.use(require('ssb-blobs'))
55
-
.use(require('ssb-invite'))
55
+
.use(require('./plugins/invite'))
56
56
.use(require('ssb-local'))
57
57
.use(require('ssb-logging'))
58
58
.use(require('ssb-query'))
···
157
157
158
158
159
159
160
-
161
-
162
-
+1
-4
lib/validators.js
+1
-4
lib/validators.js
···
1
1
var valid = require('muxrpc-validation')
2
2
var zerr = require('zerr')
3
-
var ref = require('ssb-ref')
3
+
var ref = require('../plugins/ref')
4
4
5
5
// errors
6
6
var MissingAttr = zerr('Usage', 'Param % must have a .% of type "%"')
···
226
226
return AttrType(n, 'hops', 'number')
227
227
}
228
228
})
229
-
230
-
231
-
232
229
233
230
234
231
+4
-1
npm-shrinkwrap.json
+4
-1
npm-shrinkwrap.json
···
28
28
"muxrpcli": "3",
29
29
"mv": "^2.1.1",
30
30
"osenv": "^0.1.5",
31
+
"flumeview-reduce": "^1.3.0",
32
+
"layered-graph": "^1.1.1",
31
33
"pull-cat": "~1.1.5",
32
34
"pull-file": "^1.0.0",
35
+
"pull-flatmap": "0.0.1",
33
36
"pull-many": "~1.0.6",
37
+
"pull-notify": "^0.1.1",
34
38
"pull-pushable": "^2.2.0",
35
39
"pull-stream": "^3.6.2",
36
40
"rimraf": "^2.4.2",
···
41
45
"ssb-config": "^3.2.5",
42
46
"ssb-db": "^20.0.1",
43
47
"ssb-ebt": "^5.6.1",
44
-
"ssb-friends": "^4.1.0",
45
48
"ssb-gossip": "^1.1.0",
46
49
"ssb-invite": "^2.1.2",
47
50
"ssb-keys": "^7.1.1",
-2
package.json
-2
package.json
+38
plugins/friends/contacts.js
+38
plugins/friends/contacts.js
···
1
+
var Reduce = require('flumeview-reduce')
2
+
var isFeed = require('../ref').isFeed
3
+
//track contact messages, follow, unfollow, block
4
+
5
+
module.exports = function (sbot, createLayer, config) {
6
+
7
+
var layer = createLayer('contacts')
8
+
var initial = false
9
+
var hops = {}
10
+
hops[sbot.id] = 0
11
+
var index = sbot._flumeUse('contacts2', Reduce(9, function (g, data) {
12
+
if(!g) g = {}
13
+
14
+
var from = data.value.author
15
+
var to = data.value.content.contact
16
+
var value =
17
+
data.value.content.blocking || data.value.content.flagged ? -1 :
18
+
data.value.content.following === true ? 1
19
+
: -2
20
+
21
+
if(isFeed(from) && isFeed(to)) {
22
+
if(initial) {
23
+
layer(from, to, value)
24
+
}
25
+
g[from] = g[from] || {}
26
+
g[from][to] = value
27
+
}
28
+
return g
29
+
}))
30
+
31
+
// trigger flume machinery to wait until index is ready,
32
+
// otherwise there is a race condition when rebuilding the graph.
33
+
index.get(function (err, g) {
34
+
initial = true
35
+
layer(g || {})
36
+
})
37
+
}
38
+
+77
plugins/friends/help.js
+77
plugins/friends/help.js
···
1
+
var SourceDest = {
2
+
source: {
3
+
type: 'FeedId',
4
+
description: 'the feed which posted the contact message'
5
+
},
6
+
dest: {
7
+
type: 'FeedId',
8
+
description: 'the feed the contact message pointed at'
9
+
}
10
+
}
11
+
12
+
var HopsOpts = {
13
+
start: {
14
+
type: 'FeedId',
15
+
description: 'feed at which to start traversing graph from, default to your own feed id'
16
+
},
17
+
max: {
18
+
type: 'number',
19
+
description: 'include feeds less than or equal to this number of hops',
20
+
}
21
+
}
22
+
23
+
var StreamOpts = Object.assign(
24
+
HopsOpts, {
25
+
live: {
26
+
type: 'boolean',
27
+
description: 'include real time results, defaults to false',
28
+
},
29
+
old: {
30
+
type: 'boolean',
31
+
description: 'include old results, defaults to true'
32
+
}
33
+
}
34
+
)
35
+
36
+
module.exports = {
37
+
description: 'track what feeds are following or blocking each other',
38
+
commands: {
39
+
isFollowing: {
40
+
type: 'async',
41
+
description: 'check if a feed is following another',
42
+
args: SourceDest
43
+
},
44
+
isBlocking: {
45
+
type: 'async',
46
+
description: 'check if a feed is blocking another',
47
+
args: SourceDest
48
+
},
49
+
hops: {
50
+
type: 'async',
51
+
description: 'dump the map of hops, show all feeds, and how far away they are from start',
52
+
args: HopsOpts
53
+
},
54
+
hopStream: {
55
+
type: 'source',
56
+
description: 'stream real time changes to hops. output is series of `{<FeedId>: <hops>,...}` merging these together will give the output of hops',
57
+
args: StreamOpts
58
+
},
59
+
60
+
get: {
61
+
type: 'async',
62
+
description: 'dump internal state of friends plugin, the stored follow graph',
63
+
args: {}
64
+
},
65
+
stream: {
66
+
type: 'source',
67
+
description: 'stream real time changes to graph. of hops, output of `get`, followed by {from: <FeedId>, to: <FeedId>: value: true|null|false, where true represents follow, null represents unfollow, and false represents block.',
68
+
args: StreamOpts
69
+
},
70
+
createFriendStream: {
71
+
type: 'source',
72
+
description: 'same as `stream`, but output is series of `{id: <FeedId>, hops: <hops>}`',
73
+
args: StreamOpts
74
+
},
75
+
}
76
+
}
77
+
+128
plugins/friends/index.js
+128
plugins/friends/index.js
···
1
+
'use strict'
2
+
var LayeredGraph = require('layered-graph')
3
+
var pull = require('pull-stream')
4
+
var isFeed = require('../ref').isFeed
5
+
// friends plugin
6
+
// methods to analyze the social graph
7
+
// maintains a 'follow' and 'flag' graph
8
+
9
+
exports.name = 'friends'
10
+
exports.version = '1.0.0'
11
+
exports.manifest = {
12
+
hopStream: 'source',
13
+
onEdge: 'sync',
14
+
isFollowing: 'async',
15
+
isBlocking: 'async',
16
+
hops: 'async',
17
+
help: 'sync',
18
+
// createLayer: 'sync', // not exposed over RPC as returns a function
19
+
get: 'async', // classic (previously marked legacy)
20
+
createFriendStream: 'source', // classic (previously marked legacy)
21
+
stream: 'source' // classic (previously marked legacy)
22
+
}
23
+
24
+
//mdm.manifest(apidoc)
25
+
26
+
exports.init = function (sbot, config) {
27
+
var max = config.friends && config.friends.hops || config.replicate && config.replicate.hops || 3
28
+
var layered = LayeredGraph({max: max, start: sbot.id})
29
+
30
+
function isFollowing (opts, cb) {
31
+
layered.onReady(function () {
32
+
var g = layered.getGraph()
33
+
cb(null, g[opts.source] && g[opts.source][opts.dest] >= 0)
34
+
})
35
+
}
36
+
37
+
function isBlocking (opts, cb) {
38
+
layered.onReady(function () {
39
+
var g = layered.getGraph()
40
+
cb(null, Math.round(g[opts.source] && g[opts.source][opts.dest]) == -1)
41
+
})
42
+
}
43
+
44
+
//opinion: do not authorize peers blocked by this node.
45
+
sbot.auth.hook(function (fn, args) {
46
+
var self = this
47
+
isBlocking({source: sbot.id, dest: args[0]}, function (err, blocked) {
48
+
if(blocked)
49
+
args[1](new Error('client is blocked'))
50
+
else fn.apply(self, args)
51
+
})
52
+
})
53
+
54
+
if(!sbot.replicate)
55
+
throw new Error('ssb-friends expects a replicate plugin to be available')
56
+
57
+
// opinion: replicate with everyone within max hops (max passed to layered above ^)
58
+
pull(
59
+
layered.hopStream({live: true, old: true}),
60
+
pull.drain(function (data) {
61
+
if(data.sync) return
62
+
for(var k in data) {
63
+
sbot.replicate.request(k, data[k] >= 0)
64
+
}
65
+
})
66
+
)
67
+
68
+
require('./contacts')(sbot, layered.createLayer, config)
69
+
70
+
var classic = require('./legacy')(layered)
71
+
72
+
//opinion: pass the blocks to replicate.block
73
+
setImmediate(function () {
74
+
var block = (sbot.replicate && sbot.replicate.block) || (sbot.ebt && sbot.ebt.block)
75
+
if(block) {
76
+
function handleBlockUnlock(from, to, value) {
77
+
if (value === false) block(from, to, true)
78
+
else block(from, to, false)
79
+
}
80
+
pull(
81
+
classic.stream({live: true}),
82
+
pull.drain(function (contacts) {
83
+
if(!contacts) return
84
+
85
+
if (isFeed(contacts.from) && isFeed(contacts.to)) { // live data
86
+
handleBlockUnlock(contacts.from, contacts.to, contacts.value)
87
+
} else { // initial data
88
+
for (var from in contacts) {
89
+
var relations = contacts[from]
90
+
for (var to in relations)
91
+
handleBlockUnlock(from, to, relations[to])
92
+
}
93
+
}
94
+
})
95
+
)
96
+
}
97
+
})
98
+
99
+
return {
100
+
hopStream: layered.hopStream,
101
+
onEdge: layered.onEdge,
102
+
isFollowing: isFollowing,
103
+
isBlocking: isBlocking,
104
+
105
+
// expose createLayer, so that other plugins may express relationships
106
+
createLayer: layered.createLayer,
107
+
108
+
// classic, debugging
109
+
hops: function (opts, cb) {
110
+
layered.onReady(function () {
111
+
if(isFunction(opts))
112
+
cb = opts, opts = {}
113
+
cb(null, layered.getHops(opts))
114
+
})
115
+
},
116
+
help: function () { return require('./help') },
117
+
// classic
118
+
get: classic.get,
119
+
createFriendStream: classic.createFriendStream,
120
+
stream: classic.stream,
121
+
}
122
+
}
123
+
124
+
// helpers
125
+
126
+
function isFunction (f) {
127
+
return 'function' === typeof f
128
+
}
+91
plugins/friends/legacy.js
+91
plugins/friends/legacy.js
···
1
+
var FlatMap = require('pull-flatmap')
2
+
var pull = require('pull-stream')
3
+
var Notify = require('pull-notify')
4
+
5
+
module.exports = function (layered) {
6
+
7
+
function mapGraph (g, fn) {
8
+
var _g = {}
9
+
for(var j in g)
10
+
for(var k in g[j]) {
11
+
_g[j] = _g[j] || {}
12
+
_g[j][k] = fn(g[j][k])
13
+
}
14
+
return _g
15
+
}
16
+
17
+
function map(o, fn) {
18
+
var _o = {}
19
+
for(var k in o) _o[k] = fn(o[k])
20
+
return _o
21
+
}
22
+
23
+
function toLegacyValue (v) {
24
+
//follow and same-as are shown as follow
25
+
//-2 is unfollow, -1 is block.
26
+
return v >= 0 ? true : v === -2 ? null : v === -1 ? false : null
27
+
}
28
+
29
+
var streamNotify = Notify()
30
+
layered.onEdge(function (j,k,v) {
31
+
streamNotify({from:j, to:k, value:toLegacyValue(v)})
32
+
})
33
+
34
+
return {
35
+
createFriendStream: function (opts) {
36
+
var first = true
37
+
return pull(
38
+
layered.hopStream(opts),
39
+
FlatMap(function (change) {
40
+
var a = []
41
+
for(var k in change)
42
+
if(!first || change[k] >= 0)
43
+
a.push(opts && opts.meta ? {id: k, hops: change[k]} : k)
44
+
first = false
45
+
return a
46
+
})
47
+
)
48
+
},
49
+
get: function (opts, cb) {
50
+
if(!cb)
51
+
cb = opts, opts = {}
52
+
layered.onReady(function () {
53
+
var value = layered.getGraph()
54
+
//opts is used like this in ssb-ws
55
+
if(opts && opts.source) {
56
+
value = value[opts.source]
57
+
if(value && opts.dest)
58
+
cb(null, toLegacyValue(value[opts.dest]))
59
+
else
60
+
cb(null, map(value, toLegacyValue))
61
+
}
62
+
else if( opts && opts.dest) {
63
+
var _value = {}
64
+
for(var k in value)
65
+
if('undefined' !== typeof value[k][opts.dest])
66
+
_value[k] = value[k][opts.dest]
67
+
cb(null, map(_value, toLegacyValue))
68
+
}
69
+
else
70
+
cb(null, mapGraph(value, toLegacyValue))
71
+
})
72
+
},
73
+
stream: function () {
74
+
var source = streamNotify.listen()
75
+
layered.onReady(function () {
76
+
source.push(mapGraph(layered.getGraph(), toLegacyValue))
77
+
})
78
+
return source
79
+
}
80
+
}
81
+
}
82
+
83
+
function isEmpty (obj) {
84
+
return typeof obj === 'object' &&
85
+
Object.keys(obj).length === 0
86
+
}
87
+
88
+
89
+
90
+
91
+
+75
plugins/invite/README.md
+75
plugins/invite/README.md
···
1
+
# ssb-invite (classic, vendored)
2
+
3
+
Invite-token system, mainly used for pubs. Creates invite codes as one of ways of onboarding.
4
+
5
+
Generally this ends being used for pubs:
6
+
7
+
- Users choose a pub from a [list of pubs](https://github.com/ssbc/ssb-server/wiki/Pub-Servers).
8
+
- The chosen pub gives out an invite code to the user via the pub's website.
9
+
- The user installs a Scuttlebutt client and copy and paste the invite code into the client's "accept invite" prompt.
10
+
- The pub validates the invite code and follows back the new user, making them visible to other Scuttlebutt users.
11
+
12
+
This project vendors the classic invite system used by secure-scuttlebot, and treats it as the canonical invite mechanism for this codebase.
13
+
14
+
## api
15
+
16
+
### create: async
17
+
18
+
Create a new invite code.
19
+
20
+
```shell
21
+
create {n} [{note}, {external}]
22
+
```
23
+
24
+
```javascript
25
+
create(n[, note, external], cb)
26
+
```
27
+
28
+
This produces an invite-code which encodes the ssb-server instance's public address, and a keypair seed.
29
+
The keypair seed is used to generate a keypair, which is then used to authenticate a connection with the ssb-server instance.
30
+
The ssb-server instance will then grant access to the `use` call.
31
+
32
+
- `n` (number): How many times the invite can be used before it expires.
33
+
- `note` (string): A note to associate with the invite code. The ssb-server instance will
34
+
include this note in the follow message that it creates when `use` is
35
+
called.
36
+
- `external` (string): An external hostname to use
37
+
38
+
39
+
### accept: async
40
+
41
+
Use an invite code.
42
+
43
+
- invitecode (string)
44
+
45
+
```bash
46
+
accept {invitecode}
47
+
```
48
+
49
+
```js
50
+
accept(invitecode, cb)
51
+
```
52
+
53
+
This connects to the server address encoded in the invite-code, then calls `use()` on the server.
54
+
It will cause the server to follow the local user.
55
+
56
+
57
+
### use: async
58
+
59
+
Use an invite code created by this ssb-server instance (advanced function).
60
+
61
+
```bash
62
+
use --feed {feedid}
63
+
```
64
+
65
+
```javascript
66
+
use({ feed: }, cb)
67
+
```
68
+
69
+
This commands the receiving server to follow the given feed.
70
+
71
+
An invite-code encodes the ssb-server instance's address, and a keypair seed.
72
+
The keypair seed must be used to generate a keypair, then authenticate a connection with the ssb-server instance, in order to use this function.
73
+
74
+
- `feed` (feedid): The feed the server should follow.
75
+
+38
plugins/invite/help.js
+38
plugins/invite/help.js
···
1
+
module.exports = {
2
+
description: 'accept and create pub/followbot invites',
3
+
commands: {
4
+
create: {
5
+
type: 'async',
6
+
description: 'create an invite code, must be called on a pub with a static address',
7
+
args: {
8
+
uses: {
9
+
type: 'number',
10
+
description: 'number of times this invite may be used'
11
+
},
12
+
modern: {
13
+
type: 'boolean',
14
+
description: 'return an invite which is also a multiserver address. all modern invites have a single use'
15
+
},
16
+
external: {
17
+
type: 'Host',
18
+
description: "overide the pub's host name in the invite code"
19
+
},
20
+
note: {
21
+
type: 'any',
22
+
description: 'metadata to attach to invite. this will be included in the contact message when the pub accepts this code'
23
+
}
24
+
}
25
+
},
26
+
accept: {
27
+
type: 'async',
28
+
description: 'accept an invite, connects to the pub, requests invite, then follows pub if successful',
29
+
args: {
30
+
invite: {
31
+
type: 'InviteCode',
32
+
description: 'the invite code to accept'
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
+288
plugins/invite/index.js
+288
plugins/invite/index.js
···
1
+
'use strict'
2
+
var valid = require('muxrpc-validation')({})
3
+
var crypto = require('crypto')
4
+
var ssbKeys = require('ssb-keys')
5
+
var cont = require('cont')
6
+
var explain = require('explain-error')
7
+
var ip = require('ip')
8
+
var fs = require('fs')
9
+
var ref = require('../ref')
10
+
var level = require('level')
11
+
var sublevel = require('level-sublevel/bytewise')
12
+
var path = require('path')
13
+
14
+
var createClient = require('ssb-client/client')
15
+
16
+
// invite plugin
17
+
// adds methods for producing invite-codes,
18
+
// which peers can use to command your server to follow them.
19
+
20
+
function isFunction (f) {
21
+
return 'function' === typeof f
22
+
}
23
+
24
+
function isString (s) {
25
+
return 'string' === typeof s
26
+
}
27
+
28
+
function isObject(o) {
29
+
return o && 'object' === typeof o
30
+
}
31
+
32
+
function isNumber(n) {
33
+
return 'number' === typeof n && !isNaN(n)
34
+
}
35
+
36
+
module.exports = {
37
+
name: 'invite',
38
+
version: '1.0.0',
39
+
manifest: require('./manifest.json'),
40
+
permissions: {
41
+
master: {allow: ['create']},
42
+
//temp: {allow: ['use']}
43
+
},
44
+
init: function (server, config) {
45
+
var codes = {}, codesDB
46
+
if(server.sublevel)
47
+
codesDB = server.sublevel('codes')
48
+
else {
49
+
var db = sublevel(level(path.join(config.path, 'db'), {
50
+
valueEncoding: 'json'
51
+
}))
52
+
codesDB = db.sublevel('codes')
53
+
}
54
+
//add an auth hook.
55
+
server.auth.hook(function (fn, args) {
56
+
var pubkey = args[0], cb = args[1]
57
+
58
+
// run normal authentication
59
+
fn(pubkey, function (err, auth) {
60
+
if(err || auth) return cb(err, auth)
61
+
62
+
// if no rights were already defined for this pubkey
63
+
// check if the pubkey is one of our invite codes
64
+
codesDB.get(pubkey, function (_, code) {
65
+
//disallow if this invite has already been used.
66
+
if(code && (code.used >= code.total)) cb()
67
+
else cb(null, code && code.permissions)
68
+
})
69
+
})
70
+
})
71
+
72
+
function getInviteAddress () {
73
+
return (config.allowPrivate
74
+
? server.getAddress('public') || server.getAddress('local') || server.getAddress('private')
75
+
: server.getAddress('public')
76
+
)
77
+
}
78
+
79
+
return {
80
+
create: valid.async(function (opts, cb) {
81
+
opts = opts || {}
82
+
if(isNumber(opts))
83
+
opts = {uses: opts}
84
+
else if(isObject(opts)) {
85
+
if(opts.modern)
86
+
opts.uses = 1
87
+
}
88
+
else if(isFunction(opts))
89
+
cb = opts, opts = {}
90
+
91
+
var addr = getInviteAddress()
92
+
if(!addr) return cb(new Error(
93
+
'no address available for creating an invite,'+
94
+
'configuration needed for server.\n'+
95
+
'see: https://github.com/ssbc/ssb-config/#connections'
96
+
))
97
+
addr = addr.split(';').shift()
98
+
var host = ref.parseAddress(addr).host
99
+
if(typeof host !== 'string') {
100
+
return cb(new Error('Could not parse host portion from server address:' + addr))
101
+
}
102
+
103
+
if (opts.external)
104
+
host = opts.external
105
+
106
+
if(!config.allowPrivate && (ip.isPrivate(host) || 'localhost' === host || host === ''))
107
+
return cb(new Error('Server has no public ip address, '
108
+
+ 'cannot create useable invitation'))
109
+
110
+
//this stuff is SECURITY CRITICAL
111
+
//so it should be moved into the main app.
112
+
//there should be something that restricts what
113
+
//permissions the plugin can create also:
114
+
//it should be able to diminish it's own permissions.
115
+
116
+
// generate a key-seed and its key
117
+
var seed = crypto.randomBytes(32)
118
+
var keyCap = ssbKeys.generate('ed25519', seed)
119
+
120
+
// store metadata under the generated pubkey
121
+
var owner = server.id
122
+
codesDB.put(keyCap.id, {
123
+
id: keyCap.id,
124
+
total: +opts.uses || 1,
125
+
note: opts.note,
126
+
used: 0,
127
+
permissions: {allow: ['invite.use', 'getAddress'], deny: null}
128
+
}, function (err) {
129
+
// emit the invite code: our server address, plus the key-seed
130
+
if(err) cb(err)
131
+
else if(opts.modern) {
132
+
var ws_addr = getInviteAddress().split(';').sort(function (a, b) {
133
+
return +/^ws/.test(b) - +/^ws/.test(a)
134
+
}).shift()
135
+
136
+
137
+
if(!/^ws/.test(ws_addr)) throw new Error('not a ws address:'+ws_addr)
138
+
cb(null, ws_addr+':'+seed.toString('base64'))
139
+
}
140
+
else {
141
+
addr = ref.parseAddress(addr)
142
+
cb(null, [opts.external ? opts.external : addr.host, addr.port, addr.key].join(':') + '~' + seed.toString('base64'))
143
+
}
144
+
})
145
+
}, 'number|object', 'string?'),
146
+
use: valid.async(function (req, cb) {
147
+
var rpc = this
148
+
149
+
// fetch the code
150
+
codesDB.get(rpc.id, function(err, invite) {
151
+
if(err) return cb(err)
152
+
153
+
// check if we're already following them
154
+
server.friends.get(function (err, follows) {
155
+
// server.friends.all('follow', function(err, follows) {
156
+
// if(hops[req.feed] == 1)
157
+
if (follows && follows[server.id] && follows[server.id][req.feed])
158
+
return cb(new Error('already following'))
159
+
160
+
// although we already know the current feed
161
+
// it's included so that request cannot be replayed.
162
+
if(!req.feed)
163
+
return cb(new Error('feed to follow is missing'))
164
+
165
+
if(invite.used >= invite.total)
166
+
return cb(new Error('invite has expired'))
167
+
168
+
invite.used ++
169
+
170
+
//never allow this to be used again
171
+
if(invite.used >= invite.total) {
172
+
invite.permissions = {allow: [], deny: null}
173
+
}
174
+
//TODO
175
+
//okay so there is a small race condition here
176
+
//if people use a code massively in parallel
177
+
//then it may not be counted correctly...
178
+
//this is not a big enough deal to fix though.
179
+
//-dominic
180
+
181
+
// update code metadata
182
+
codesDB.put(rpc.id, invite, function (err) {
183
+
server.emit('log:info', ['invite', rpc.id, 'use', req])
184
+
185
+
// follow the user
186
+
server.publish({
187
+
type: 'contact',
188
+
contact: req.feed,
189
+
following: true,
190
+
pub: true,
191
+
note: invite.note || undefined
192
+
}, cb)
193
+
})
194
+
})
195
+
})
196
+
}, 'object'),
197
+
accept: valid.async(function (invite, cb) {
198
+
// remove surrounding quotes, if found
199
+
if(isObject(invite))
200
+
invite = invite.invite
201
+
202
+
if (invite.charAt(0) === '"' && invite.charAt(invite.length - 1) === '"')
203
+
invite = invite.slice(1, -1)
204
+
var opts
205
+
// connect to the address in the invite code
206
+
// using a keypair generated from the key-seed in the invite code
207
+
var modern = false
208
+
if(ref.isInvite(invite)) { //legacy ivite
209
+
if(ref.isLegacyInvite(invite)) {
210
+
var parts = invite.split('~')
211
+
opts = ref.parseAddress(parts[0])//.split(':')
212
+
//convert legacy code to multiserver invite code.
213
+
var protocol = 'net:'
214
+
if (opts.host.endsWith(".onion"))
215
+
protocol = 'onion:'
216
+
invite = protocol+opts.host+':'+opts.port+'~shs:'+opts.key.slice(1, -8)+':'+parts[1]
217
+
}
218
+
else
219
+
modern = true
220
+
}
221
+
222
+
opts = ref.parseAddress(ref.parseInvite(invite).remote)
223
+
function connect (cb) {
224
+
createClient({
225
+
keys: true, //use seed from invite instead.
226
+
remote: invite,
227
+
config: config,
228
+
manifest: {invite: {use: 'async'}, getAddress: 'async'}
229
+
}, cb)
230
+
}
231
+
232
+
// retry 3 times, with timeouts.
233
+
// This is an UGLY hack to get the test/invite.js to pass
234
+
// it's a race condition, I think because the server isn't ready
235
+
// when it connects?
236
+
237
+
function retry (fn, cb) {
238
+
var n = 0
239
+
;(function next () {
240
+
var start = Date.now()
241
+
fn(function (err, value) {
242
+
n++
243
+
if(n >= 3) cb(err, value)
244
+
else if(err) setTimeout(next, 500 + (Date.now()-start)*n)
245
+
else cb(null, value)
246
+
})
247
+
})()
248
+
}
249
+
250
+
retry(connect, function (err, rpc) {
251
+
252
+
if(err) return cb(explain(err, 'could not connect to server'))
253
+
254
+
// command the peer to follow me
255
+
rpc.invite.use({ feed: server.id }, function (err, msg) {
256
+
if(err) return cb(explain(err, 'invite not accepted'))
257
+
258
+
// follow and announce the pub
259
+
cont.para([
260
+
cont(server.publish)({
261
+
type: 'contact',
262
+
following: true,
263
+
autofollow: true,
264
+
contact: opts.key
265
+
}),
266
+
(
267
+
opts.host
268
+
? cont(server.publish)({
269
+
type: 'pub',
270
+
address: opts
271
+
})
272
+
: function (cb) { cb() }
273
+
)
274
+
])
275
+
(function (err, results) {
276
+
if(err) return cb(err)
277
+
rpc.close()
278
+
rpc.close()
279
+
//ignore err if this is new style invite
280
+
if(server.gossip) server.gossip.add(ref.parseInvite(invite).remote, 'seed')
281
+
cb(null, results)
282
+
})
283
+
})
284
+
})
285
+
}, 'string')
286
+
}
287
+
}
288
+
}
+6
plugins/invite/manifest.json
+6
plugins/invite/manifest.json
+324
plugins/ref/index.js
+324
plugins/ref/index.js
···
1
+
var isCanonicalBase64 = require('is-canonical-base64')
2
+
var isDomain = require('is-valid-domain')
3
+
var Querystring = require('querystring')
4
+
var ip = require('ip')
5
+
6
+
var parseLinkRegex = /^((@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+)(\?(.+))?$/
7
+
var linkRegex = exports.linkRegex = /^(@|%|&)[A-Za-z0-9\/+]{43}=\.[\w\d]+$/
8
+
var feedIdRegex = exports.feedIdRegex = isCanonicalBase64('@', '\.(?:sha256|ed25519)', 32)
9
+
var blobIdRegex = exports.blobIdRegex = isCanonicalBase64('&', '\.sha256', 32)
10
+
var msgIdRegex = exports.msgIdRegex = isCanonicalBase64('%', '\.sha256', 32)
11
+
12
+
var extractRegex = /([@%&][A-Za-z0-9\/+]{43}=\.[\w\d]+)/
13
+
14
+
var MultiServerAddress = require('multiserver-address')
15
+
16
+
function isMultiServerAddress (str) {
17
+
//a http url fits into the multiserver scheme,
18
+
//but all ssb address must have a transport and a transform
19
+
//so check there is at least one unescaped ~ in the address
20
+
return MultiServerAddress.check(str) && /[^!][~]/.test(str)
21
+
}
22
+
23
+
function isIP (s) {
24
+
return ip.isV4Format(s) || ip.isV6Format(s)
25
+
}
26
+
27
+
var isInteger = Number.isInteger
28
+
var DEFAULT_PORT = 8008
29
+
30
+
function isString (s) {
31
+
return 'string' === typeof s
32
+
}
33
+
34
+
var isHost = function (addr) {
35
+
if (!isString(addr)) return
36
+
addr = addr.replace(/^wss?:\/\//, '')
37
+
return (isIP(addr)) || isDomain(addr) || addr === 'localhost'
38
+
}
39
+
40
+
var isPort = function (p) {
41
+
return isInteger(p) && p <= 65536
42
+
}
43
+
44
+
function isObject (o) {
45
+
return o && 'object' === typeof o && !Array.isArray(o)
46
+
}
47
+
48
+
var isFeedId = exports.isFeed = exports.isFeedId =
49
+
function (data) {
50
+
return isString(data) && feedIdRegex.test(data)
51
+
}
52
+
53
+
var isMsgId = exports.isMsg = exports.isMsgId =
54
+
function (data) {
55
+
return isString(data) && msgIdRegex.test(data)
56
+
}
57
+
58
+
var isBlobId = exports.isBlob = exports.isBlobId =
59
+
function (data) {
60
+
return isString(data) && blobIdRegex.test(data)
61
+
}
62
+
63
+
var isLink = exports.isLink =
64
+
function (data) {
65
+
if (!isString(data)) return false
66
+
var index = data.indexOf('?')
67
+
data = ~index ? data.substring(0, index) : data
68
+
return isString(data) && (isFeedId(data) || isMsgId(data) || isBlobId(data))
69
+
}
70
+
71
+
exports.isBlobLink = function (s) {
72
+
return s[0] === '&' && isLink(s)
73
+
}
74
+
75
+
exports.isMsgLink = function (s) {
76
+
return s[0] === '%' && isLink(s)
77
+
}
78
+
79
+
var normalizeChannel = exports.normalizeChannel =
80
+
function (data) {
81
+
if (typeof data === 'string') {
82
+
data = data.toLowerCase().replace(/\s|,|\.|\?|!|<|>|\(|\)|\[|\]|"|#/g, '')
83
+
if (data.length > 0 && data.length < 30) {
84
+
return data
85
+
}
86
+
}
87
+
}
88
+
89
+
// Classic ssb-ref had deprecation wrappers around some APIs (console.trace).
90
+
// For "classic" usage in this repo, we keep the APIs but drop the warnings.
91
+
function deprecate (name, fn) {
92
+
return fn
93
+
}
94
+
95
+
var parseMultiServerAddress = function (data) {
96
+
if (!isString(data)) return false
97
+
if (!MultiServerAddress.check(data)) return false
98
+
99
+
var addr = MultiServerAddress.decode(data)
100
+
addr = addr.find(function (address) {
101
+
if (!address[0]) return false
102
+
if (!address[1]) return false
103
+
return /^(net|wss?|onion)$/.test(address[0].name) && /^shs/.test(address[1].name)
104
+
})
105
+
if (!Array.isArray(addr)) {
106
+
return false
107
+
}
108
+
var port = +addr[0].data.pop() //last item always port, to handle ipv6
109
+
110
+
//preserve protocol type on websocket addresses
111
+
var host = (/^wss?$/.test(addr[0].name) ? addr[0].name + ':' : '') + addr[0].data.join(':')
112
+
var key = '@' + addr[1].data[0] + '.ed25519'
113
+
var seed = addr[1].data[2]
114
+
// allow multiserver addresses that are not currently understood!
115
+
if (!(isHost(host) && isPort(+port) && isFeedId(key))) return false
116
+
var address = {
117
+
host: host,
118
+
port: port,
119
+
key: key
120
+
}
121
+
122
+
if (seed) address.seed = seed
123
+
return address
124
+
}
125
+
126
+
var toLegacyAddress = parseMultiServerAddress
127
+
exports.toLegacyAddress = deprecate('ssb-ref.toLegacyAddress', toLegacyAddress)
128
+
129
+
var isLegacyAddress = exports.isLegacyAddress = function (addr) {
130
+
return isObject(addr) && isHost(addr.host) && isPort(addr.port) && isFeedId(addr.key)
131
+
}
132
+
133
+
var toMultiServerAddress = exports.toMultiServerAddress = function (addr) {
134
+
if (MultiServerAddress.check(addr)) return addr
135
+
if (!isPort(addr.port)) throw new Error('ssb-ref.toMultiServerAddress - invalid port:' + addr.port)
136
+
if (!isHost(addr.host)) throw new Error('ssb-ref.toMultiServerAddress - invalid host:' + addr.host)
137
+
if (!isFeedId(addr.key)) throw new Error('ssb-ref.toMultiServerAddress - invalid key:' + addr.key)
138
+
139
+
return (
140
+
/^wss?:/.test(addr.host) ? addr.host
141
+
: /\.onion$/.test(addr.host) ? 'onion:' + addr.host
142
+
: 'net:' + addr.host
143
+
) + ':' + addr.port + '~shs:' + addr.key.substring(1, addr.key.indexOf('.'))
144
+
}
145
+
146
+
var isAddress = exports.isAddress = function (data) {
147
+
var host, port, id
148
+
if (isObject(data)) {
149
+
id = data.key; host = data.host; port = data.port
150
+
} else if (!isString(data)) return false
151
+
else if (isMultiServerAddress(data)) return true
152
+
else {
153
+
var parts = data.split(':')
154
+
id = parts.pop(); port = parts.pop(); host = parts.join(':')
155
+
}
156
+
return (
157
+
isFeedId(id) && isPort(+port) &&
158
+
isHost(host)
159
+
)
160
+
}
161
+
162
+
//This is somewhat fragile, because maybe non-shs protocols get added...
163
+
//it would be better to treat all addresses as opaque or have multiserver handle
164
+
//extraction of a signing key from the address.
165
+
var getKeyFromAddress = exports.getKeyFromAddress = function (addr) {
166
+
if (addr.key) return addr.key
167
+
var data = MultiServerAddress.decode(addr)
168
+
if (!data) return
169
+
for (var k in data) {
170
+
var address = data[k]
171
+
for (var j in address) {
172
+
var protocol = address[j]
173
+
if (/^shs/.test(protocol.name)) //forwards compatible with future shs versions...
174
+
return '@' + protocol.data[0] + '.ed25519'
175
+
}
176
+
}
177
+
}
178
+
179
+
var parseAddress = function (e) {
180
+
if (isString(e)) {
181
+
if (~e.indexOf('~'))
182
+
return parseMultiServerAddress(e)
183
+
var parts = e.split(':')
184
+
var id = parts.pop(), port = parts.pop(), host = parts.join(':')
185
+
var e = {
186
+
host: host,
187
+
port: +(port || DEFAULT_PORT),
188
+
key: id
189
+
}
190
+
return e
191
+
}
192
+
return e
193
+
}
194
+
exports.parseAddress = deprecate('ssb-ref.parseAddress', parseAddress)
195
+
196
+
var toAddress = exports.toAddress = function (e) {
197
+
e = parseAddress(e)
198
+
e.port = e.port || DEFAULT_PORT
199
+
e.host = e.host || 'localhost'
200
+
return e
201
+
}
202
+
203
+
var legacyInviteRegex = /^[A-Za-z0-9\/+]{43}=$/
204
+
var legacyInviteFixerRegex = /#.*$/
205
+
var isLegacyInvite = exports.isLegacyInvite =
206
+
function (data) {
207
+
if (!isString(data)) return false
208
+
data = data.replace(legacyInviteFixerRegex, '')
209
+
var parts = data.split('~')
210
+
return parts.length == 2 && isAddress(parts[0]) && legacyInviteRegex.test(parts[1])
211
+
}
212
+
213
+
var isMultiServerInvite = exports.isMultiServerInvite =
214
+
function (data) {
215
+
if (!isString(data)) return false
216
+
return !!parseMultiServerInvite(data)
217
+
}
218
+
219
+
var isInvite = exports.isInvite =
220
+
function (data) {
221
+
if (!isString(data)) return false
222
+
return isLegacyInvite(data) || isMultiServerInvite(data)
223
+
}
224
+
225
+
exports.parseLink = function parseBlob (ref) {
226
+
var match = parseLinkRegex.exec(ref)
227
+
if (match && match[1]) {
228
+
if (match[3]) {
229
+
var query = Querystring.parse(match[4])
230
+
// unbox keys have a '+' in them that is parsed into a ' ', this changes it back
231
+
if (isString(query.unbox)) query.unbox = query.unbox.replace(/ /g, '+')
232
+
return { link: match[1], query: query }
233
+
} else {
234
+
return { link: match[1] }
235
+
}
236
+
}
237
+
}
238
+
239
+
function parseLegacyInvite (invite) {
240
+
var redirect = invite.split('#')
241
+
invite = redirect.shift()
242
+
var parts = invite.split('~')
243
+
var addr = toAddress(parts[0])
244
+
//convert legacy code to multiserver invite code.
245
+
var remote = toMultiServerAddress(addr)
246
+
invite = remote + ':' + parts[1]
247
+
return {
248
+
invite: remote + ':' + parts[1],
249
+
key: addr.key,
250
+
redirect: redirect.length ? '#' + redirect.join('#') : null
251
+
}
252
+
}
253
+
254
+
function parseMultiServerInvite (invite) {
255
+
var redirect = invite.split('#')
256
+
if (!redirect.length) return null
257
+
258
+
invite = redirect.shift()
259
+
var addr = toLegacyAddress(invite)
260
+
if (!addr) return null
261
+
delete addr.seed
262
+
return {
263
+
invite: invite,
264
+
remote: toMultiServerAddress(addr),
265
+
key: addr.key,
266
+
redirect: redirect.length ? '#' + redirect.join('#') : null
267
+
}
268
+
}
269
+
270
+
exports.parseLegacyInvite = deprecate('ssb-ref.parseLegacyInvite', parseLegacyInvite)
271
+
exports.parseMultiServerInvite = deprecate('ssb-ref.parseMultiServerInvite', parseMultiServerInvite)
272
+
273
+
exports.parseInvite = deprecate('ssb-ref.parseInvite', function (invite) {
274
+
return (
275
+
isLegacyInvite(invite)
276
+
? parseLegacyInvite(invite)
277
+
: isMultiServerInvite(invite)
278
+
? parseMultiServerInvite(invite)
279
+
: null
280
+
)
281
+
})
282
+
283
+
exports.type =
284
+
function (id) {
285
+
if (!isString(id)) return false
286
+
var c = id.charAt(0)
287
+
if (c == '@' && isFeedId(id))
288
+
return 'feed'
289
+
else if (c == '%' && isMsgId(id))
290
+
return 'msg'
291
+
else if (c == '&' && isBlobId(id))
292
+
return 'blob'
293
+
else if (isAddress(id)) return 'address'
294
+
else if (isInvite(id)) return 'invite'
295
+
else
296
+
return false
297
+
}
298
+
299
+
exports.extract =
300
+
function (data) {
301
+
if (!isString(data))
302
+
return false
303
+
304
+
var _data = data
305
+
306
+
var res = extractRegex.exec(_data)
307
+
if (res) {
308
+
return res && res[0]
309
+
} else {
310
+
try { _data = decodeURIComponent(data) }
311
+
catch (e) {} // this may fail if it's not encoded, so don't worry if it does
312
+
_data = _data.replace(/&/g, '&')
313
+
314
+
var res = extractRegex.exec(_data)
315
+
return res && res[0]
316
+
}
317
+
}
318
+
319
+
320
+
321
+
322
+
323
+
324
+
+1
-1
test/caps.js
+1
-1
test/caps.js