├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bun.lockb ├── index.js ├── lib └── test.html ├── package.json ├── public ├── stats.html └── style.css └── serve-bun.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bunsAndGuns.code-workspace 3 | .vscode 4 | .env 5 | radata -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun 2 | 3 | WORKDIR /app 4 | COPY package.json package.json 5 | COPY bun.lockb bun.lockb 6 | RUN bun install 7 | COPY . . 8 | EXPOSE 3000 9 | 10 | ENTRYPOINT ["bun", "index.js"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 James 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gundb-relay 2 | Simple gun db relay running on Bun.js 3 | 4 | Connect and test with this public peer/relay: https://plankton-app-6qfp3.ondigitalocean.app/ 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgibson14/bunsAndGuns/a5e2dc6e22195c4bde6d19b763cc0c025b46fa74/bun.lockb -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Bun from 'bun' 2 | import Gun from 'gun/gun' 3 | import SEA from 'gun/sea' 4 | globalThis.GUN = Gun 5 | // This is mostly copied from 'gun/lib/server.js' 6 | var u; 7 | Gun.on('opt', function(root){ 8 | if(u === root.opt.super){ root.opt.super = true } 9 | if(u === root.opt.faith){ root.opt.faith = true } // HNPERF: This should probably be off, but we're testing performance improvements, please audit. 10 | root.opt.log = root.opt.log || Gun.log; 11 | this.to.next(root); 12 | }) 13 | require('gun/lib/store.js'); 14 | require('gun/lib/rfs.js'); 15 | require('gun/lib/rs3.js'); 16 | // replacing wire.js with this file 17 | // require('./wire'); 18 | 19 | require('gun/sea.js') 20 | require('gun/axe.js') 21 | require('gun/lib/stats.js'); 22 | // multicast doesn't work with Bun because dgram is not supported yet: https://bun.sh/docs/runtime/nodejs-apis 23 | // require('gun/lib/multicast.js'); 24 | 25 | // 26 | import httpConfig from './serve-bun.js' 27 | const server = Bun.serve( httpConfig ) 28 | 29 | const env = process.env 30 | const VALID_KEY = env.VALID_KEY 31 | const USE_RADISK = env.DISABLE_RADISK !== 'true' 32 | const USE_AXE = env.DISABLE_AXE !== 'true' 33 | const gun = globalThis.gunInstance = Gun({ 34 | web: server, 35 | axe: USE_AXE, 36 | localStorage: false, 37 | radisk: USE_RADISK, 38 | peers: env?.PEERS?.split(',') || [], 39 | validKey: VALID_KEY, 40 | multicast: false, 41 | pack: 599000000 * 0.3, 42 | s: { 43 | key: env.AWS_ACCESS_KEY_ID, // AWS Access Key 44 | secret: env.AWS_SECRET_ACCESS_KEY, // AWS Secret Token 45 | bucket: env.AWS_S3_BUCKET // The bucket you want to save into 46 | } 47 | }) 48 | console.log('Running Bun and Gun') -------------------------------------------------------------------------------- /lib/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | This Relay: Raw JSON Stats    15 | Monitor Relay    16 | Github 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | Add a header token to every message (optional) 25 |
26 | 27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 | 55 |
56 | 57 |

58 | 59 |

60 | 61 |

62 |
63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 | Logs here: 72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 197 | 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bunsAndGuns", 3 | "module": "index.js", 4 | "type": "module", 5 | "scripts": { 6 | "start": "bun run index.js", 7 | "dev": "bun run --watch index.js", 8 | "debug": "bun run --inspect-brk index.js" 9 | }, 10 | "dependencies": { 11 | "aws-sdk": "^2.1454.0", 12 | "gun": "^0.2020.1239" 13 | } 14 | } -------------------------------------------------------------------------------- /public/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 31 | 32 |
0 peers 0 min 0 nodes 0 hours 0 block 0 stack
33 | 34 | 35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 125 | 126 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | position: relative; 5 | line-height: 1.5; 6 | font-size: 18pt; 7 | f-ont-size: max(18pt, 2?vw); 8 | } 9 | 10 | div, ul, ol, li, p, span, form, button, input, textarea, img { 11 | margin: 0; 12 | padding: 0; 13 | position: relative; 14 | vertical-align: inherit; 15 | -webkit-transition: all 0.3s; 16 | transition: all 0.3s; 17 | box-sizing: border-box; 18 | font: inherit; 19 | } 20 | 21 | a, button, input, textarea { 22 | background: inherit; 23 | border: inherit; 24 | color: inherit; 25 | text-decoration: inherit; 26 | outline: none; 27 | } 28 | input:not([type=button]):not([type=submit]), textarea { 29 | width: 100%; 30 | } 31 | 32 | a:focus, button:focus, input[type=button]:focus, input[type=submit]:focus { 33 | animation: pulse 2s infinite; 34 | } 35 | 36 | ul, li { 37 | list-style: none; 38 | } 39 | 40 | p { 41 | padding: 0; 42 | } 43 | p + p { 44 | padding-top: 0; 45 | } 46 | 47 | [contenteditable=true]:empty:before { 48 | content: attr(placeholder); 49 | } 50 | ::placeholder, .hint { 51 | color: inherit; 52 | opacity: 0.3; 53 | } 54 | 55 | .model, .none { display: none } 56 | .hide { 57 | opacity: 0; 58 | visibility: hidden; 59 | transition: all 2s; 60 | } 61 | 62 | .full { 63 | width: 100%; 64 | min-height: 100vh; 65 | } 66 | .max { 67 | max-width: 48em; 68 | } 69 | .min { 70 | min-width: 12em; 71 | } 72 | .pad { 73 | width: 95%; 74 | margin: 5% auto; 75 | max-width: 48em; 76 | min-width: 12em; 77 | } 78 | 79 | .row { 80 | width: 100%; 81 | } 82 | .row::after { 83 | content: ""; 84 | display: block; 85 | clear: both; 86 | } 87 | .col { 88 | max-width: 24em; 89 | min-width: 12em; 90 | } 91 | 92 | .center { 93 | text-align: center; 94 | vertical-align: middle; 95 | margin-left: auto; 96 | margin-right: auto; 97 | } 98 | .right { 99 | float: right; 100 | text-align: right; 101 | } 102 | .left { 103 | float: left; 104 | text-align: left; 105 | } 106 | .mid { 107 | margin-left: auto; 108 | margin-right: auto; 109 | } 110 | .top { 111 | vertical-align: top; 112 | } 113 | .low { 114 | vertical-align: bottom; 115 | } 116 | 117 | .rim { margin: 1%; } 118 | .gap { 119 | padding: 3%; 120 | padding: clamp(0.5em, 3%, 1.5em); 121 | } 122 | .stack { line-height: 0; } 123 | .crack { margin-bottom: 1%; } 124 | .sit { margin-bottom: 0; } 125 | 126 | .focus { 127 | margin-left: auto; 128 | margin-right: auto; 129 | float: none; 130 | clear: both; 131 | } 132 | 133 | .leak { overflow: visible; } 134 | .hold { overflow: hidden; } 135 | 136 | .act { 137 | /*display: block;*/ 138 | font-weight: normal; 139 | text-decoration: none; 140 | -webkit-transition: all 0.3s; 141 | transition: all 0.3s; 142 | cursor: pointer; 143 | } 144 | 145 | .unit, .symbol { 146 | display: inline-block; 147 | vertical-align: inherit; 148 | } 149 | .sap { border-radius: 0.1em; } 150 | .jot { border-bottom: 1px dashed #95B2CA; } 151 | 152 | .loud { 153 | font-size: 150%; 154 | } 155 | .shout { 156 | font-size: 36pt; 157 | font-size: 6.5vmax; 158 | } 159 | 160 | .red { background: #ea3224; } 161 | .green { background: #33cc33; } 162 | .blue { background: #4D79D8; } 163 | .yellow { background: #d3a438; } 164 | .black { background: black; } 165 | .white { background: white; } 166 | 167 | .shade { background: rgba(0%, 0%, 0%, 0.1); } 168 | .tint { background: rgba(100%, 100%, 100%, 0.1); } 169 | 170 | .redt { color: #ea3224; } 171 | .greent { color: #33cc33; } 172 | .bluet { color: #4D79D8; } 173 | .yellowt { color: #d3a438; } 174 | .blackt { color: black; } 175 | .whitet { color: white; } 176 | 177 | .hue { 178 | background: #4D79D8; 179 | -webkit-animation: hue 900s infinite; 180 | animation: hue 900s infinite; 181 | } @keyframes hue { 182 | 0% {background-color: #4D79D8;} 183 | 25% {background-color: #33cc33;} 184 | 50% {background-color: #d3a438;} 185 | 75% {background-color: #ea3224;} 186 | 100% {background-color: #4D79D8;} 187 | } @-webkit-keyframes hue { 188 | 0% {background-color: #4D79D8;} 189 | 25% {background-color: #33cc33;} 190 | 50% {background-color: #d3a438;} 191 | 75% {background-color: #ea3224;} 192 | 100% {background-color: #4D79D8;} 193 | } 194 | 195 | .huet { 196 | color: #4D79D8; 197 | -webkit-animation: huet 900s infinite; 198 | animation: huet 900s infinite; 199 | } @keyframes huet { 200 | 0% {color: #4D79D8;} 201 | 25% {color: #33cc33;} 202 | 50% {color: #d3a438;} 203 | 75% {color: #ea3224;} 204 | 100% {color: #4D79D8;} 205 | } @-webkit-keyframes huet { 206 | 0% {color: #4D79D8;} 207 | 25% {color: #33cc33;} 208 | 50% {color: #d3a438;} 209 | 75% {color: #ea3224;} 210 | 100% {color: #4D79D8;} 211 | } 212 | 213 | .hue2 { 214 | background: #ea3224; 215 | -webkit-animation: hue2 900s infinite; 216 | animation: hue2 900s infinite; 217 | } @keyframes hue2 { 218 | 0% {background-color: #ea3224;} 219 | 25% {background-color: #4D79D8;} 220 | 50% {background-color: #33cc33;} 221 | 75% {background-color: #d3a438;} 222 | 100% {background-color: #ea3224;} 223 | } @-webkit-keyframes hue2 { 224 | 0% {background-color: #ea3224;} 225 | 25% {background-color: #4D79D8;} 226 | 50% {background-color: #33cc33;} 227 | 75% {background-color: #d3a438;} 228 | 100% {background-color: #ea3224;} 229 | } 230 | 231 | .huet2 { 232 | color: #ea3224; 233 | -webkit-animation: huet2 900s infinite; 234 | animation: huet2 900s infinite; 235 | } @keyframes huet2 { 236 | 0% {color: #ea3224;} 237 | 25% {color: #4D79D8;} 238 | 50% {color: #33cc33;} 239 | 75% {color: #d3a438;} 240 | 100% {color: #ea3224;} 241 | } @-webkit-keyframes huet2 { 242 | 0% {color: #ea3224;} 243 | 25% {color: #4D79D8;} 244 | 50% {color: #33cc33;} 245 | 75% {color: #d3a438;} 246 | 100% {color: #ea3224;} 247 | } 248 | 249 | .hue3 { 250 | background: #33cc33; 251 | -webkit-animation: hue3 900s infinite; 252 | animation: hue3 900s infinite; 253 | } @keyframes hue3 { 254 | 0% {background-color: #33cc33;} 255 | 25% {background-color: #d3a438;} 256 | 50% {background-color: #ea3224;} 257 | 75% {background-color: #4D79D8;} 258 | 100% {background-color: #33cc33;} 259 | } @-webkit-keyframes hue3 { 260 | 0% {background-color: #33cc33;} 261 | 25% {background-color: #d3a438;} 262 | 50% {background-color: #ea3224;} 263 | 75% {background-color: #4D79D8;} 264 | 100% {background-color: #33cc33;} 265 | } 266 | 267 | .huet3 { 268 | color: #33cc33; 269 | -webkit-animation: huet3 900s infinite; 270 | animation: huet3 900s infinite; 271 | } @keyframes huet3 { 272 | 0% {color: #33cc33;} 273 | 25% {color: #d3a438;} 274 | 50% {color: #ea3224;} 275 | 75% {color: #4D79D8;} 276 | 100% {color: #33cc33;} 277 | } @-webkit-keyframes huet3 { 278 | 0% {color: #33cc33;} 279 | 25% {color: #d3a438;} 280 | 50% {color: #ea3224;} 281 | 75% {color: #4D79D8;} 282 | 100% {color: #33cc33;} 283 | } 284 | 285 | .hue4 { 286 | background: #d3a438; 287 | -webkit-animation: hue4 900s infinite; 288 | animation: hue4 900s infinite; 289 | } @keyframes hue4 { 290 | 0% {background-color: #d3a438;} 291 | 25% {background-color: #ea3224;} 292 | 50% {background-color: #4D79D8;} 293 | 75% {background-color: #33cc33;} 294 | 100% {background-color: #d3a438;} 295 | } @-webkit-keyframes hue4 { 296 | 0% {background-color: #d3a438;} 297 | 25% {background-color: #ea3224;} 298 | 50% {background-color: #4D79D8;} 299 | 75% {background-color: #33cc33;} 300 | 100% {background-color: #d3a438;} 301 | } 302 | 303 | .huet4 { 304 | color: #d3a438; 305 | -webkit-animation: huet4 900s infinite; 306 | animation: huet4 900s infinite; 307 | } @keyframes huet4 { 308 | 0% {color: #d3a438;} 309 | 25% {color: #ea3224;} 310 | 50% {color: #4D79D8;} 311 | 75% {color: #33cc33;} 312 | 100% {color: #d3a438;} 313 | } @-webkit-keyframes huet4 { 314 | 0% {color: #d3a438;} 315 | 25% {color: #ea3224;} 316 | 50% {color: #4D79D8;} 317 | 75% {color: #33cc33;} 318 | 100% {color: #d3a438;} 319 | } 320 | 321 | .pulse { 322 | animation: pulse 2s infinite; 323 | } @keyframes pulse { 324 | 0% {opacity: 1;} 325 | 50% {opacity: 0.5;} 326 | 100% {opacity: 1;} 327 | } 328 | 329 | .joy { 330 | width: 100px; 331 | height: 100px; 332 | position: absolute; 333 | background: url(https://cdn.jsdelivr.net/npm/gun/examples/pop.png) no-repeat; 334 | background-position: -2800px 0; 335 | pointer-events: none; 336 | z-index: 999999999; 337 | animation: joy 1s steps(28); 338 | } @keyframes joy { 339 | 0% {background-position: 0 0;} 340 | 100% {background-position: -2800px 0;} 341 | } 342 | 343 | .visually-hidden { 344 | border: 0; 345 | clip: rect(1px, 1px, 1px, 1px); 346 | height: 1px; 347 | margin: -1px; 348 | overflow: hidden; 349 | padding: 0; 350 | position: absolute; 351 | width: 1px; 352 | } -------------------------------------------------------------------------------- /serve-bun.js: -------------------------------------------------------------------------------- 1 | import Gun from 'gun/gun' 2 | const perMessageDeflate = process.env.DISABLE_WEBSOCKET_COMPRESSION !== 'true' 3 | Gun.on('opt', function(root){ 4 | var opt = root.opt; 5 | if(false === opt.ws || opt.once){ 6 | this.to.next(root); 7 | return; 8 | } 9 | 10 | opt.mesh = opt.mesh || Gun.Mesh(root); 11 | opt.WebSocket = opt.WebSocket || require('ws'); 12 | var ws = opt.ws = opt.ws || {}; 13 | ws.path = ws.path || '/gun'; 14 | // if we DO need an HTTP server, then choose ws specific one or GUN default one. 15 | if(!ws.noServer){ 16 | ws.server = ws.server || opt.web; 17 | if(!ws.server){ this.to.next(root); return } // ugh, bug fix for @jamierez & unstoppable ryan. 18 | } 19 | // ws.web = ws.web || new opt.WebSocket.Server(ws); // we still need a WS server. 20 | 21 | this.to.next(root); 22 | }); 23 | function parseCookies (rc) { 24 | var list = {} 25 | 26 | rc && rc.split(';').forEach(function( cookie ) { 27 | var parts = cookie.split('='); 28 | list[parts.shift().trim()] = decodeURI(parts.join('=')); 29 | }); 30 | 31 | return list; 32 | } 33 | export default { 34 | port: process.env.PORT || 3000, 35 | async fetch(req, server) { 36 | const url = new URL(req.url); 37 | const cookies = parseCookies(req.headers.get("Cookie")); 38 | console.log('req.header: ', req.headers ) 39 | if( server.upgrade(req, { 40 | headers: { 41 | "Set-Cookie": `SessionId=${ crypto.randomUUID().slice(0,13) }`, 42 | }, 43 | data: { 44 | headers: req.headers, 45 | createdAt: Date.now(), 46 | channelId: url.searchParams.get("channelId"), 47 | sessionId: cookies["SessionId"], 48 | gunRoot: globalThis.gunInstance 49 | }, 50 | })){ 51 | return 52 | } 53 | if (url.pathname === "/") return new Response(Bun.file("./lib/test.html")); 54 | if (url.pathname === "/stats.json") return new Response( Bun.file("./node_modules/gun/stats.radata") ); 55 | if (url.pathname.includes('/public')){ 56 | const file = url.pathname.split('/').pop() 57 | console.log('load public file', file) 58 | return new Response(Bun.file(`./public/${ file}`)) 59 | } 60 | return new Response(`404!`); 61 | }, 62 | websocket: { 63 | open(ws) { 64 | const gunOpt = ws.data.gunRoot._.opt 65 | const origin = ws.data.headers.get('origin') 66 | console.log(`WS opened`, ws.data.createdAt, ws.data.sessionId, origin ) 67 | console.STAT && ((console.STAT.sites || (console.STAT.sites = { [ origin ]:0 }))[ origin ] += 1); 68 | let peer = {wire: ws} 69 | gunOpt.mesh.hi( peer ); 70 | ws.data.peer = peer 71 | ws.data.heartbeat = setInterval(function heart(){ if(!gunOpt.peers[peer.id]){ return } try{ ws.send("[]") }catch(e){}} , 1000 * 20) 72 | 73 | }, // a socket is opened 74 | async message(ws, message) { 75 | const gunOpt = ws.data.gunRoot._.opt 76 | console.log(`Received ${message}`); 77 | gunOpt.mesh.hear( message, ws.data.peer); 78 | }, // a message is received 79 | close(ws, code, message) { 80 | const gunOpt = ws.data.gunRoot._.opt 81 | console.log(`WS closed`, ws.data.headers.origin) 82 | const origin = ws.data.headers.get('origin') 83 | console.STAT.sites[ origin ] -= 1; 84 | gunOpt.mesh.bye( ws.data.peer ); 85 | clearInterval( ws.data.heartbeat ) 86 | }, // a socket is closed 87 | drain(ws) { 88 | console.log(`WS drain`, ws) 89 | }, // the socket is ready to receive more data 90 | // enable compression and decompression 91 | perMessageDeflate, 92 | }, 93 | error(error) { 94 | return new Response(`
${error}\n${error.stack}
`, { 95 | headers: { 96 | "Content-Type": "text/html", 97 | }, 98 | }); 99 | }, 100 | }; --------------------------------------------------------------------------------