secure-scuttlebot classic

add the rest bridge

Changed files
+263
plugins
rest-bridge
+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 + }