+10
README.md
+10
README.md
···
133
133
- **http://localhost:8989/**
134
134
135
135
The server will serve the Patchbay Lite UI and expose the necessary endpoints for the client. You can now browse and publish messages from your browser.
136
+
If you need a different host/port, add this to `~/.ssb/config`:
137
+
138
+
```json
139
+
{
140
+
"ws": {
141
+
"host": "127.0.0.1",
142
+
"port": 8989
143
+
}
144
+
}
145
+
```
136
146
137
147
## Decent Bundle
138
148
+6
bin.js
+6
bin.js
···
136
136
var Config = require('ssb-config/inject')
137
137
var config = Config(process.env.ssb_appname, overrides)
138
138
139
+
if (config.ws !== false) {
140
+
if (!config.ws || typeof config.ws !== 'object') config.ws = {}
141
+
if (typeof config.ws.port !== 'number') config.ws.port = 8989
142
+
if (typeof config.ws.host !== 'string') config.ws.host = '127.0.0.1'
143
+
}
144
+
139
145
if (config.keys.curve === 'k256')
140
146
throw new Error('k256 curves are no longer supported,'+
141
147
'please delete' + path.join(config.path, 'secret'))
+4
-65
lib/frontend.js
+4
-65
lib/frontend.js
···
11
11
if (!sbot.ws || typeof sbot.ws.use !== 'function')
12
12
return {}
13
13
14
-
var publicDir = path.join(__dirname, '..', 'public')
15
14
var patchbayDir = path.join(__dirname, '..', 'patchbay')
16
15
var patchbayIndex = path.join(patchbayDir, 'build', 'index.html')
17
16
var hasPatchbay = fs.existsSync(patchbayIndex)
···
38
37
sbot.ws.use(function (req, res, next) {
39
38
var url = req.url.split('?')[0]
40
39
41
-
if (req.method === 'POST' && url === '/publish') {
42
-
if (!sbot.add)
43
-
return next()
44
-
45
-
var body = ''
46
-
req.on('data', function (data) {
47
-
body += data
48
-
if (body.length > 1e6)
49
-
req.connection.destroy()
50
-
})
51
-
req.on('end', function () {
52
-
var data
53
-
try { data = JSON.parse(body) }
54
-
catch (e) {
55
-
res.writeHead(400, {'Content-Type': 'application/json'})
56
-
return res.end(JSON.stringify({error: 'invalid json'}))
57
-
}
58
-
59
-
var msg = data && data.msg
60
-
if (!msg || typeof msg !== 'object') {
61
-
res.writeHead(400, {'Content-Type': 'application/json'})
62
-
return res.end(JSON.stringify({error: 'missing msg'}))
63
-
}
64
-
65
-
sbot.add(msg, function (err, saved) {
66
-
if (err) {
67
-
res.writeHead(500, {'Content-Type': 'application/json'})
68
-
return res.end(JSON.stringify({error: err.message}))
69
-
}
70
-
res.writeHead(200, {'Content-Type': 'application/json'})
71
-
res.end(JSON.stringify(saved, null, 2))
72
-
})
73
-
})
74
-
return
75
-
}
76
-
77
40
if (req.method === 'OPTIONS' && url === '/blobs/add') {
78
41
res.writeHead(204, {
79
42
'Access-Control-Allow-Origin': '*',
···
159
122
})
160
123
}
161
124
162
-
if (url === '/log.json') {
163
-
if (!sbot.createLogStream)
164
-
return next()
165
-
166
-
res.writeHead(200, {'Content-Type': 'application/json'})
167
-
return pull(
168
-
sbot.createLogStream({ limit: 100, reverse: true }),
169
-
pull.collect(function (err, msgs) {
170
-
if (err) {
171
-
res.statusCode = 500
172
-
return res.end(JSON.stringify({ error: err.message }))
173
-
}
174
-
res.end(JSON.stringify(msgs, null, 2))
175
-
})
176
-
)
177
-
}
178
-
179
125
if (url === '/') url = '/index.html'
180
126
181
127
var relPath = url.replace(/^\/+/, '')
182
-
var filePath = path.join(publicDir, relPath)
183
128
var patchbayPath = path.join(patchbayDir, relPath)
184
129
185
-
if (filePath.indexOf(publicDir) !== 0 || relPath.indexOf('..') !== -1)
186
-
filePath = null
187
130
if (patchbayPath.indexOf(patchbayDir) !== 0 || relPath.indexOf('..') !== -1)
188
131
patchbayPath = null
189
132
190
-
fs.stat(filePath || patchbayPath, function (err, stat) {
191
-
if (!err && stat && stat.isFile() && filePath) return serveFile(res, filePath)
192
-
193
-
if (!patchbayPath) return next()
133
+
if (!patchbayPath) return next()
194
134
195
-
fs.stat(patchbayPath, function (err2, stat2) {
196
-
if (err2 || !stat2 || !stat2.isFile()) return next()
197
-
serveFile(res, patchbayPath)
198
-
})
135
+
fs.stat(patchbayPath, function (err2, stat2) {
136
+
if (err2 || !stat2 || !stat2.isFile()) return next()
137
+
serveFile(res, patchbayPath)
199
138
})
200
139
})
201
140
-303
public/app.js
-303
public/app.js
···
1
-
;(function () {
2
-
var statusEl = document.getElementById('status')
3
-
var keysEl = document.getElementById('keys')
4
-
var feedEl = document.getElementById('feed')
5
-
var composeTextEl = document.getElementById('compose-text')
6
-
var composeSendEl = document.getElementById('compose-send')
7
-
8
-
var currentKeys = null
9
-
var currentFeedId = null
10
-
var signKeyPromise = null
11
-
var feedState = {}
12
-
13
-
function setStatus (msg) {
14
-
if (statusEl) statusEl.textContent = msg
15
-
}
16
-
17
-
function setKeys (obj) {
18
-
if (!keysEl) return
19
-
keysEl.textContent = JSON.stringify(obj, null, 2)
20
-
}
21
-
22
-
function clearFeed () {
23
-
if (!feedEl) return
24
-
while (feedEl.firstChild) feedEl.removeChild(feedEl.firstChild)
25
-
}
26
-
27
-
function loadKeys () {
28
-
try {
29
-
var raw = localStorage.getItem('ssb_browser_keys')
30
-
if (!raw) return null
31
-
return JSON.parse(raw)
32
-
} catch (err) {
33
-
console.error('failed to parse stored keys', err)
34
-
return null
35
-
}
36
-
}
37
-
38
-
function saveKeys (keys) {
39
-
try {
40
-
localStorage.setItem('ssb_browser_keys', JSON.stringify(keys))
41
-
} catch (err) {
42
-
console.error('failed to persist keys', err)
43
-
}
44
-
}
45
-
46
-
function toBase64 (arr) {
47
-
var s = ''
48
-
for (var i = 0; i < arr.length; i++) {
49
-
s += String.fromCharCode(arr[i])
50
-
}
51
-
return btoa(s)
52
-
}
53
-
54
-
function fromBase64 (str) {
55
-
var bin = atob(str)
56
-
var len = bin.length
57
-
var arr = new Uint8Array(len)
58
-
for (var i = 0; i < len; i++) {
59
-
arr[i] = bin.charCodeAt(i)
60
-
}
61
-
return arr
62
-
}
63
-
64
-
function generateKeysWithWebCrypto () {
65
-
if (!window.crypto || !window.crypto.subtle || !window.crypto.subtle.generateKey) {
66
-
return Promise.reject(new Error('WebCrypto Ed25519 not available'))
67
-
}
68
-
69
-
// This assumes a modern browser with Ed25519 in SubtleCrypto.
70
-
// You may need to adjust algorithm name depending on engine.
71
-
return window.crypto.subtle.generateKey(
72
-
{ name: 'Ed25519' },
73
-
true,
74
-
['sign', 'verify']
75
-
).then(function (keyPair) {
76
-
return Promise.all([
77
-
window.crypto.subtle.exportKey('raw', keyPair.publicKey),
78
-
window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
79
-
]).then(function (exported) {
80
-
var pub = new Uint8Array(exported[0])
81
-
var priv = new Uint8Array(exported[1])
82
-
83
-
// These are raw Ed25519 key bytes (pub) and PKCS#8 (priv), base64-encoded.
84
-
return {
85
-
curve: 'ed25519',
86
-
public: toBase64(pub),
87
-
private: toBase64(priv)
88
-
}
89
-
})
90
-
})
91
-
}
92
-
93
-
function ensureKeys () {
94
-
var existing = loadKeys()
95
-
if (existing && existing.public && existing.private) {
96
-
setStatus('Loaded existing keys from localStorage')
97
-
setKeys(existing)
98
-
currentKeys = existing
99
-
currentFeedId = '@' + existing.public + '.ed25519'
100
-
return ensureSignKey(existing).then(function () {
101
-
return existing
102
-
})
103
-
}
104
-
105
-
setStatus('Generating new keypair in browser…')
106
-
return generateKeysWithWebCrypto().then(function (keys) {
107
-
saveKeys(keys)
108
-
setStatus('Generated and stored new keypair')
109
-
setKeys(keys)
110
-
currentKeys = keys
111
-
currentFeedId = '@' + keys.public + '.ed25519'
112
-
return ensureSignKey(keys).then(function () {
113
-
return keys
114
-
})
115
-
}).catch(function (err) {
116
-
console.error('failed to generate keys', err)
117
-
setStatus('Failed to generate keys: ' + err.message)
118
-
setKeys({})
119
-
return null
120
-
})
121
-
}
122
-
123
-
function ensureSignKey (keys) {
124
-
if (signKeyPromise) return signKeyPromise
125
-
if (!window.crypto || !window.crypto.subtle || !window.crypto.subtle.importKey) {
126
-
return Promise.reject(new Error('WebCrypto Ed25519 not available'))
127
-
}
128
-
129
-
var privB64 = keys.private
130
-
var pkcs8 = fromBase64(privB64).buffer
131
-
132
-
signKeyPromise = window.crypto.subtle.importKey(
133
-
'pkcs8',
134
-
pkcs8,
135
-
{ name: 'Ed25519' },
136
-
false,
137
-
['sign']
138
-
)
139
-
140
-
return signKeyPromise
141
-
}
142
-
143
-
function loadLog () {
144
-
setStatus('Keys ready; loading log…')
145
-
146
-
fetch('/log.json')
147
-
.then(function (res) { return res.json() })
148
-
.then(function (msgs) {
149
-
if (!Array.isArray(msgs)) msgs = []
150
-
feedState = {}
151
-
clearFeed()
152
-
153
-
// messages are newest-first (reverse: true), so first seen is latest
154
-
msgs.forEach(function (msg) {
155
-
var value = msg && msg.value
156
-
var author = value && value.author
157
-
if (!author) return
158
-
if (!feedState[author]) {
159
-
feedState[author] = {
160
-
id: msg.key,
161
-
sequence: value.sequence,
162
-
timestamp: value.timestamp
163
-
}
164
-
}
165
-
166
-
if (!feedEl || !value || !value.content) return
167
-
168
-
var text = value.content && value.content.text
169
-
var type = value.content && value.content.type
170
-
var ts = value.timestamp
171
-
var date = ts ? new Date(ts).toISOString() : ''
172
-
173
-
var postDiv = document.createElement('div')
174
-
postDiv.className = 'post'
175
-
176
-
var metaDiv = document.createElement('div')
177
-
metaDiv.className = 'post-meta'
178
-
metaDiv.textContent = (type || 'message') + ' · ' + (author || '') + (date ? ' · ' + date : '')
179
-
180
-
var textDiv = document.createElement('div')
181
-
textDiv.className = 'post-text'
182
-
textDiv.textContent = text || JSON.stringify(value.content)
183
-
184
-
postDiv.appendChild(metaDiv)
185
-
postDiv.appendChild(textDiv)
186
-
feedEl.appendChild(postDiv)
187
-
})
188
-
setStatus('Keys ready; log loaded')
189
-
})
190
-
.catch(function (err) {
191
-
console.error('failed to load log', err)
192
-
clearFeed()
193
-
if (feedEl) {
194
-
var errDiv = document.createElement('div')
195
-
errDiv.className = 'post'
196
-
errDiv.textContent = 'failed to load log: ' + err.message
197
-
feedEl.appendChild(errDiv)
198
-
}
199
-
setStatus('Keys ready; failed to load log')
200
-
})
201
-
}
202
-
203
-
function signMessage (unsignedMsg) {
204
-
return ensureSignKey(currentKeys).then(function (key) {
205
-
var json = JSON.stringify(unsignedMsg, null, 2)
206
-
var encoder = new TextEncoder()
207
-
var bytes = encoder.encode(json)
208
-
return window.crypto.subtle.sign(
209
-
{ name: 'Ed25519' },
210
-
key,
211
-
bytes
212
-
).then(function (sigBuf) {
213
-
var sigBytes = new Uint8Array(sigBuf)
214
-
var sigB64 = toBase64(sigBytes)
215
-
return sigB64 + '.sig.ed25519'
216
-
})
217
-
})
218
-
}
219
-
220
-
function buildUnsignedMessage (text) {
221
-
if (!currentKeys || !currentFeedId) {
222
-
throw new Error('Browser keys not ready')
223
-
}
224
-
225
-
var state = feedState[currentFeedId] || null
226
-
var ts = Date.now()
227
-
if (state && ts <= state.timestamp) ts = state.timestamp + 1
228
-
229
-
return {
230
-
previous: state ? state.id : null,
231
-
sequence: state ? state.sequence + 1 : 1,
232
-
author: currentFeedId,
233
-
timestamp: ts,
234
-
hash: 'sha256',
235
-
content: {
236
-
type: 'post',
237
-
text: text
238
-
}
239
-
}
240
-
}
241
-
242
-
function publishFromBrowser (text) {
243
-
var unsigned = buildUnsignedMessage(text)
244
-
245
-
return signMessage(unsigned).then(function (sig) {
246
-
var msg = {
247
-
previous: unsigned.previous,
248
-
sequence: unsigned.sequence,
249
-
author: unsigned.author,
250
-
timestamp: unsigned.timestamp,
251
-
hash: unsigned.hash,
252
-
content: unsigned.content,
253
-
signature: sig
254
-
}
255
-
256
-
return fetch('/publish', {
257
-
method: 'POST',
258
-
headers: {
259
-
'Content-Type': 'application/json'
260
-
},
261
-
body: JSON.stringify({ msg: msg })
262
-
}).then(function (res) {
263
-
return res.json()
264
-
}).then(function (saved) {
265
-
return saved
266
-
})
267
-
})
268
-
}
269
-
270
-
function wireCompose () {
271
-
if (!composeSendEl || !composeTextEl) return
272
-
273
-
composeSendEl.addEventListener('click', function () {
274
-
var text = composeTextEl.value
275
-
if (!text) return
276
-
277
-
composeSendEl.disabled = true
278
-
setStatus('Publishing post…')
279
-
280
-
publishFromBrowser(text)
281
-
.then(function (msg) {
282
-
composeTextEl.value = ''
283
-
setStatus('Post published; refreshing log…')
284
-
loadLog()
285
-
})
286
-
.catch(function (err) {
287
-
console.error('failed to publish', err)
288
-
setStatus('Failed to publish: ' + err.message)
289
-
})
290
-
.then(function () {
291
-
composeSendEl.disabled = false
292
-
})
293
-
})
294
-
}
295
-
296
-
// Entry point
297
-
ensureKeys().then(function () {
298
-
loadLog()
299
-
wireCompose()
300
-
// TODO: wire these keys into a browserified ssb-client
301
-
// that connects to the ssb-ws endpoint exposed by ssb-server.
302
-
})
303
-
})()
-84
public/index.html
-84
public/index.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="utf-8">
5
-
<title>Patchbay Lite (ssb-server)</title>
6
-
<meta name="viewport" content="width=device-width, initial-scale=1">
7
-
<style>
8
-
body {
9
-
font-family: sans-serif;
10
-
margin: 0;
11
-
padding: 0;
12
-
}
13
-
header {
14
-
padding: .5em 1em;
15
-
background: #f5f5f5;
16
-
border-bottom: 1px solid #ddd;
17
-
}
18
-
#status {
19
-
font-size: .9em;
20
-
color: #666;
21
-
}
22
-
main {
23
-
display: flex;
24
-
flex-direction: column;
25
-
padding: 1em;
26
-
}
27
-
.compose {
28
-
margin-bottom: 1em;
29
-
}
30
-
.compose textarea {
31
-
width: 100%;
32
-
box-sizing: border-box;
33
-
}
34
-
.feed {
35
-
border-top: 1px solid #eee;
36
-
}
37
-
.post {
38
-
border-bottom: 1px solid #f5f5f5;
39
-
padding: .5em 0;
40
-
}
41
-
.post-meta {
42
-
font-size: .8em;
43
-
color: #888;
44
-
margin-bottom: .25em;
45
-
}
46
-
.post-text {
47
-
white-space: pre-wrap;
48
-
word-wrap: break-word;
49
-
}
50
-
#keys {
51
-
font-size: .7em;
52
-
white-space: pre-wrap;
53
-
word-wrap: break-word;
54
-
color: #999;
55
-
}
56
-
</style>
57
-
</head>
58
-
<body>
59
-
<header>
60
-
<h1>Patchbay Lite</h1>
61
-
<p id="status">Loading…</p>
62
-
</header>
63
-
<main>
64
-
<section class="compose">
65
-
<h2>Compose</h2>
66
-
<textarea id="compose-text" rows="4" cols="40" placeholder="Write a post..."></textarea>
67
-
<br>
68
-
<button id="compose-send">Publish</button>
69
-
</section>
70
-
71
-
<section class="feed">
72
-
<h2>Recent log</h2>
73
-
<div id="feed"></div>
74
-
</section>
75
-
76
-
<section>
77
-
<h3>Browser keys</h3>
78
-
<pre id="keys"></pre>
79
-
</section>
80
-
</main>
81
-
82
-
<script src="app.js"></script>
83
-
</body>
84
-
</html>