+263
plugins/rest-bridge/index.js
+263
plugins/rest-bridge/index.js
···
1
+
'use strict'
2
+
3
+
var http = require('http')
4
+
var URL = require('url').URL
5
+
var pull = require('pull-stream')
6
+
7
+
exports.name = 'rest-bridge'
8
+
exports.version = '1.0.0'
9
+
exports.manifest = {}
10
+
11
+
function parseIntParam (value, fallback) {
12
+
var num = parseInt(value, 10)
13
+
return Number.isFinite(num) && num >= 0 ? num : fallback
14
+
}
15
+
16
+
function setCors (res) {
17
+
res.setHeader('Access-Control-Allow-Origin', '*')
18
+
res.setHeader('Access-Control-Allow-Headers', 'content-type')
19
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
20
+
}
21
+
22
+
function sendJson (res, status, payload) {
23
+
setCors(res)
24
+
res.statusCode = status
25
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
26
+
res.end(JSON.stringify(payload))
27
+
}
28
+
29
+
function sendNotFound (res) {
30
+
sendJson(res, 404, {error: 'not found'})
31
+
}
32
+
33
+
function readJsonBody (req, cb) {
34
+
var limit = 2 * 1024 * 1024
35
+
var data = ''
36
+
var done = false
37
+
38
+
function finish (err, value) {
39
+
if (done) return
40
+
done = true
41
+
cb(err, value)
42
+
}
43
+
44
+
req.on('data', function (chunk) {
45
+
if (done) return
46
+
data += chunk
47
+
if (data.length > limit) {
48
+
req.destroy()
49
+
finish(new Error('request entity too large'))
50
+
}
51
+
})
52
+
53
+
req.on('error', function (err) {
54
+
finish(err)
55
+
})
56
+
57
+
req.on('end', function () {
58
+
if (done) return
59
+
if (!data) return finish(null, null)
60
+
try {
61
+
finish(null, JSON.parse(data))
62
+
} catch (err) {
63
+
finish(err)
64
+
}
65
+
})
66
+
}
67
+
68
+
exports.init = function (sbot, config) {
69
+
var restConfig = config.restBridge || {}
70
+
if (restConfig.enabled === false) return {}
71
+
72
+
var listenHost = restConfig.host || '0.0.0.0'
73
+
var listenPort = restConfig.port || 8927
74
+
75
+
function respondStatus (res) {
76
+
var status = null
77
+
if (typeof sbot.status === 'function') {
78
+
try { status = sbot.status() }
79
+
catch (err) { status = {error: err.message} }
80
+
}
81
+
82
+
var body = {
83
+
message: 'ssb rest bridge',
84
+
id: sbot.id,
85
+
public: config.keys && config.keys.public,
86
+
curve: config.keys && config.keys.curve,
87
+
host: config.host || 'localhost',
88
+
port: config.port,
89
+
rest: {host: listenHost, port: listenPort}
90
+
}
91
+
if (status) body.status = status
92
+
sendJson(res, 200, body)
93
+
}
94
+
95
+
function respondFeed (res, searchParams) {
96
+
if (typeof sbot.createLogStream !== 'function') {
97
+
return sendJson(res, 503, {error: 'log stream unavailable'})
98
+
}
99
+
100
+
var limit = parseIntParam(searchParams.get('limit'), 100)
101
+
var reverse = searchParams.get('reverse') !== 'false'
102
+
var opts = {limit: limit, reverse: reverse}
103
+
104
+
pull(
105
+
sbot.createLogStream(opts),
106
+
pull.collect(function (err, msgs) {
107
+
if (err) return sendJson(res, 500, {error: err.message})
108
+
sendJson(res, 200, msgs)
109
+
})
110
+
)
111
+
}
112
+
113
+
function respondAuthorFeed (res, author, searchParams) {
114
+
if (typeof sbot.createHistoryStream !== 'function') {
115
+
return sendJson(res, 503, {error: 'history stream unavailable'})
116
+
}
117
+
var since = parseIntParam(searchParams.get('since'), 0)
118
+
var limit = parseIntParam(searchParams.get('limit'), -1)
119
+
var opts = {
120
+
id: author,
121
+
seq: since > 0 ? since + 1 : 1,
122
+
live: false,
123
+
keys: true,
124
+
values: true
125
+
}
126
+
if (limit >= 0) opts.limit = limit
127
+
128
+
pull(
129
+
sbot.createHistoryStream(opts),
130
+
pull.collect(function (err, msgs) {
131
+
if (err) return sendJson(res, 500, {error: err.message})
132
+
sendJson(res, 200, msgs)
133
+
})
134
+
)
135
+
}
136
+
137
+
function handlePublish (req, res) {
138
+
readJsonBody(req, function (err, payload) {
139
+
if (err) return sendJson(res, 400, {error: err.message})
140
+
if (!payload || typeof payload !== 'object') {
141
+
return sendJson(res, 400, {error: 'payload must be an object'})
142
+
}
143
+
if (payload.content && typeof payload.content === 'object') {
144
+
if (typeof sbot.publish !== 'function') {
145
+
return sendJson(res, 503, {error: 'publish unavailable'})
146
+
}
147
+
return sbot.publish(payload.content, function (err2, msg) {
148
+
if (err2) return sendJson(res, 500, {error: err2.message})
149
+
sendJson(res, 200, msg)
150
+
})
151
+
}
152
+
if (payload.msg && typeof payload.msg === 'object') {
153
+
if (typeof sbot.add !== 'function') {
154
+
return sendJson(res, 503, {error: 'add unavailable'})
155
+
}
156
+
return sbot.add(payload.msg, function (err3, msg) {
157
+
if (err3) return sendJson(res, 500, {error: err3.message})
158
+
sendJson(res, 200, msg)
159
+
})
160
+
}
161
+
sendJson(res, 400, {error: 'payload must include content or msg'})
162
+
})
163
+
}
164
+
165
+
function handleFeedAppend (req, res) {
166
+
if (typeof sbot.add !== 'function') {
167
+
return sendJson(res, 503, {error: 'add unavailable'})
168
+
}
169
+
readJsonBody(req, function (err, payload) {
170
+
if (err) return sendJson(res, 400, {error: err.message})
171
+
if (!payload || typeof payload !== 'object' || typeof payload.msg !== 'object') {
172
+
return sendJson(res, 400, {error: 'missing msg'})
173
+
}
174
+
sbot.add(payload.msg, function (err2, msg) {
175
+
if (err2) return sendJson(res, 500, {error: err2.message})
176
+
sendJson(res, 200, msg)
177
+
})
178
+
})
179
+
}
180
+
181
+
function respondLog (res) {
182
+
if (typeof sbot.createLogStream !== 'function') {
183
+
return sendJson(res, 503, {error: 'log stream unavailable'})
184
+
}
185
+
pull(
186
+
sbot.createLogStream({limit: 100, reverse: true}),
187
+
pull.collect(function (err, msgs) {
188
+
if (err) return sendJson(res, 500, {error: err.message})
189
+
sendJson(res, 200, msgs)
190
+
})
191
+
)
192
+
}
193
+
194
+
function handleRequest (req, res) {
195
+
var method = (req.method || 'GET').toUpperCase()
196
+
var parsedUrl = new URL(req.url, 'http://localhost')
197
+
var pathname = parsedUrl.pathname
198
+
199
+
if (method === 'OPTIONS') {
200
+
setCors(res)
201
+
res.writeHead(204)
202
+
return res.end()
203
+
}
204
+
205
+
if (method === 'GET' && pathname === '/status') {
206
+
return respondStatus(res)
207
+
}
208
+
209
+
if (method === 'GET' && pathname === '/feed') {
210
+
return respondFeed(res, parsedUrl.searchParams)
211
+
}
212
+
213
+
if (method === 'GET' && pathname === '/log.json') {
214
+
return respondLog(res)
215
+
}
216
+
217
+
if (method === 'POST' && pathname === '/publish') {
218
+
return handlePublish(req, res)
219
+
}
220
+
221
+
if (pathname.indexOf('/feeds/') === 0) {
222
+
var author = decodeURIComponent(pathname.substring('/feeds/'.length))
223
+
if (!author) return sendJson(res, 400, {error: 'missing author'})
224
+
if (method === 'GET') {
225
+
return respondAuthorFeed(res, author, parsedUrl.searchParams)
226
+
}
227
+
if (method === 'POST') {
228
+
return handleFeedAppend(req, res)
229
+
}
230
+
}
231
+
232
+
if (method === 'GET' && pathname === '/') {
233
+
return sendJson(res, 200, {
234
+
message: 'ssb rest bridge',
235
+
endpoints: ['/status', '/feed', '/log.json', '/feeds/:id', '/publish (POST)', '/feeds/:id (POST)']
236
+
})
237
+
}
238
+
239
+
return sendNotFound(res)
240
+
}
241
+
242
+
var server = http.createServer(handleRequest)
243
+
server.listen(listenPort, listenHost, function () {
244
+
if (typeof sbot.emit === 'function') {
245
+
sbot.emit('log:info', ['rest-bridge', 'listen', listenHost + ':' + listenPort])
246
+
} else {
247
+
console.log('[rest-bridge] listening on %s:%s', listenHost, listenPort)
248
+
}
249
+
})
250
+
251
+
if (sbot.close && typeof sbot.close.hook === 'function') {
252
+
sbot.close.hook(function (fn, args) {
253
+
server.close()
254
+
return fn.apply(this, args)
255
+
})
256
+
}
257
+
258
+
return {
259
+
address: function () {
260
+
return {host: listenHost, port: listenPort}
261
+
}
262
+
}
263
+
}