├── docs ├── images │ ├── login-chart.png │ ├── screenshot.png │ ├── invites-chart.png │ ├── invites-chart.png.license │ ├── login-chart.png.license │ ├── screenshot.png.license │ ├── invites-chart.graphml.license │ └── login-chart.graphml.license ├── README.md ├── files │ ├── debian-preremove.sh │ ├── example-systemd.service │ ├── debian-postinstall.sh │ └── example-nginx.conf └── architecture.md ├── web ├── assets │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── favicon.ico.license │ │ ├── site.webmanifest.license │ │ ├── apple-touch-icon.png.license │ │ ├── favicon-16x16.png.license │ │ ├── favicon-32x32.png.license │ │ ├── android-chrome-192x192.png.license │ │ ├── android-chrome-512x512.png.license │ │ └── site.webmanifest │ ├── img │ │ ├── test-hermie.png │ │ └── test-hermie.png.license │ ├── fixfouc.css │ ├── invite-uri.js │ ├── alias-uri.js │ └── auth-withssb-uri.js ├── styles │ ├── package.json.license │ ├── package-lock.json.license │ ├── gen_dev.go │ ├── gen_prod.go │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── package.json │ └── input.css ├── templates │ ├── admin │ │ ├── menu.tmpl │ │ ├── invite-created.tmpl │ │ ├── members-show-password-reset-token.tmpl │ │ ├── members-remove-confirm.tmpl │ │ ├── aliases-revoke-confirm.tmpl │ │ ├── invite-revoke-confirm.tmpl │ │ ├── denied-keys-remove-confirm.tmpl │ │ └── notice-edit.tmpl │ ├── flashes.tmpl │ ├── error.tmpl │ ├── notice │ │ ├── show.tmpl │ │ └── list.tmpl │ ├── landing │ │ └── index.tmpl │ ├── invite │ │ ├── insert-id.tmpl │ │ ├── facade.tmpl │ │ ├── consumed.tmpl │ │ └── facade-fallback.tmpl │ ├── auth │ │ ├── fallback_sign_in.tmpl │ │ ├── withssb_server_start.tmpl │ │ └── decide_method.tmpl │ ├── alias.tmpl │ └── change-member-password.tmpl ├── i18n │ ├── dev.go │ ├── prod.go │ └── i18ntesting │ │ ├── i18n_helper_test.go │ │ └── testing.go ├── embedded_dev.go ├── handlers │ ├── language_template.go │ ├── basic_test.go │ ├── admin │ │ ├── set_language_test.go │ │ ├── aliases_test.go │ │ ├── aliases.go │ │ └── dashboard_test.go │ └── set_language_test.go ├── members │ └── testing.go ├── embedded_prod.go ├── router │ ├── auth.go │ └── complete.go └── errors │ ├── badrequest.go │ └── flashes.go ├── go.sum.license ├── muxrpc ├── test │ └── nodejs │ │ ├── package.json.license │ │ ├── package-lock.json.license │ │ ├── .gitignore │ │ ├── testscripts │ │ ├── minimal-before-setup.js │ │ ├── secretstack-modern.js │ │ ├── secretstack-legacy.js │ │ ├── legacy_client.js │ │ ├── modern_client.js │ │ ├── legacy_client_opening_tunnel.js │ │ ├── modern_client_opening_tunnel.js │ │ ├── secretstack_testplugin.js │ │ ├── modern_aliases.js │ │ ├── legacy_server.js │ │ ├── template.js │ │ ├── client.js │ │ └── client-opening-tunnel.js │ │ ├── package.json │ │ ├── aliases_test.go │ │ ├── sbot_serv.js │ │ └── sbot_client.js └── handlers │ ├── doc.go │ ├── whoami │ └── whoami.go │ ├── tunnel │ └── server │ │ ├── members.go │ │ ├── plugin.go │ │ └── attendants.go │ └── gossip │ └── ping.go ├── cmd ├── server │ ├── dev.go │ └── prod.go └── insert-user │ ├── generate-fake-id.sh │ └── main.go ├── .dockerignore ├── roomdb ├── sqlite │ ├── models │ │ ├── boil_view_names.go │ │ ├── boil_table_names.go │ │ ├── boil_queries.go │ │ ├── boil_types.go │ │ └── sqlite_upsert.go │ ├── migrations.go │ ├── new_test.go │ ├── generate_models.sh │ ├── migrations │ │ ├── 03-siwssb-tokens.sql │ │ ├── 03-config.sql │ │ ├── 02-notices.sql │ │ ├── 04-overhaul-fallback-auth.sql │ │ └── 01-consolidated.sql │ ├── sqlboiler.toml │ ├── roomconfig_test.go │ └── roomconfig.go ├── role_string.go └── privacymode_string.go ├── errors.go ├── internal ├── devtools │ ├── stringer.go │ └── counterfeiter.go ├── broadcasts │ ├── doc.go │ ├── endpoints.go │ └── attendants.go ├── randutil │ └── string.go ├── maybemod │ ├── multierror │ │ └── multierr.go │ ├── testutils │ │ ├── mergeErrChans.go │ │ └── logging.go │ └── multicloser │ │ └── multicloser.go ├── repo │ ├── repo.go │ └── secret.go ├── network │ ├── errors.go │ ├── msaddr_test.go │ ├── conntracker_acceptAll.go │ └── isserver_test.go ├── aliases │ ├── names_test.go │ ├── names.go │ ├── confirm.go │ └── confirm_test.go ├── netwraputil │ ├── spoof_test.go │ └── spoof.go ├── signinwithssb │ ├── simple_test.go │ ├── bridge_test.go │ ├── challenges.go │ └── bridge.go └── maybemuxrpc │ └── plugin.go ├── start.sh ├── .env_example ├── .reuse └── dep5 ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── docker-compose.yml ├── Dockerfile ├── .gitignore ├── LICENSE ├── LICENSES ├── MIT.txt └── Unlicense.txt ├── roomsrv ├── manifest.go ├── init_network.go ├── init_handlers.go └── init_unixsock.go ├── README.md └── go.mod /docs/images/login-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/docs/images/login-chart.png -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/images/invites-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/docs/images/invites-chart.png -------------------------------------------------------------------------------- /web/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /web/assets/img/test-hermie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/img/test-hermie.png -------------------------------------------------------------------------------- /web/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /web/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /web/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /go.sum.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /web/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/assets/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/go-ssb-room/HEAD/web/assets/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/styles/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /docs/images/invites-chart.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /docs/images/login-chart.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /docs/images/screenshot.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/favicon/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/img/test-hermie.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/styles/package-lock.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /docs/images/invites-chart.graphml.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /docs/images/login-chart.graphml.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /muxrpc/test/nodejs/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /web/assets/favicon/site.webmanifest.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /muxrpc/test/nodejs/package-lock.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /web/assets/favicon/apple-touch-icon.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/favicon/favicon-16x16.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/favicon/favicon-32x32.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/favicon/android-chrome-192x192.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /web/assets/favicon/android-chrome-512x512.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 -------------------------------------------------------------------------------- /muxrpc/test/nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | node_modules 6 | testrun 7 | -------------------------------------------------------------------------------- /cmd/server/dev.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build dev 6 | 7 | package main 8 | 9 | var development = true 10 | -------------------------------------------------------------------------------- /cmd/server/prod.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build !dev 6 | 7 | package main 8 | 9 | var development = false 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | node_moduels 6 | ssb-go-room-secrets 7 | ssb-go-room-secrets/**/* 8 | .git 9 | .env 10 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/minimal-before-setup.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | module.exports = (t, sbot, ready) => { 6 | ready() 7 | } 8 | -------------------------------------------------------------------------------- /web/assets/fixfouc.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 3 | * 4 | * SPDX-License-Identifier: CC-BY-4.0 5 | */ 6 | 7 | html { 8 | visibility: hidden; 9 | opacity: 0; 10 | } 11 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/secretstack-modern.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | module.exports = [ 6 | 'ssb-conn', 7 | 'ssb-room-client' 8 | ] 9 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/secretstack-legacy.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | module.exports = [ 6 | 'ssb-conn', 7 | 'ssb-room/tunnel/client' 8 | ] 9 | -------------------------------------------------------------------------------- /web/styles/gen_dev.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build dev 6 | 7 | package styles 8 | 9 | //go:generate npm ci 10 | //go:generate npm run compile-dev 11 | -------------------------------------------------------------------------------- /web/styles/gen_prod.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build !dev 6 | 7 | package styles 8 | 9 | //go:generate npm ci 10 | //go:generate npm run compile-prod 11 | -------------------------------------------------------------------------------- /cmd/insert-user/generate-fake-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | id=$(dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 -w0) 8 | echo "@${id}.ed25519" 9 | -------------------------------------------------------------------------------- /roomdb/sqlite/models/boil_view_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.14.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var ViewNames = struct { 7 | }{} 8 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package tunnel 6 | 7 | import "errors" 8 | 9 | var ErrShuttingDown = errors.New("go-ssb-tunnel: shutting down") // this is fine 10 | -------------------------------------------------------------------------------- /internal/devtools/stringer.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | //go:build tools 6 | // +build tools 7 | 8 | package devtools 9 | 10 | import ( 11 | _ "golang.org/x/tools/cmd/stringer" 12 | ) 13 | -------------------------------------------------------------------------------- /web/styles/postcss.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | module.exports = { 6 | plugins: { 7 | tailwindcss: {}, 8 | autoprefixer: {}, 9 | cssnano: { 10 | preset: 'default' 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | [[ -f ".env" ]] && source .env 8 | ./cmd/server/server -https-domain="${HTTPS_DOMAIN}" -repo="${REPO:-~/.ssb-go-room-secrets}" -aliases-as-subdomains="${ALIASES_AS_SUBDOMAINS}" 9 | -------------------------------------------------------------------------------- /web/assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"go-ssb-room","short_name":"go-ssb-room","icons":[{"src":"/assets/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /muxrpc/handlers/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package handlers contains the muxrpc handler packages for the room server. 6 | // 7 | // The implementation the actual multiplexing implementation is github.com/ssbc/go-muxrpc. 8 | package handlers 9 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | HTTPS_DOMAIN=yourhttpsdomainhere 6 | ALIASES_AS_SUBDOMAINS=true 7 | # uncomment variable if you want to store data in a custom directory (required for default docker-compose setup) 8 | # REPO=/ssb-go-room-secrets 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Table of contents 8 | 9 | - [**Deployment**](./deployment.md) 10 | - [**Development**](./development.md) 11 | - [**Architecture**](./architecture.md) 12 | - [**Testing**](./testing.md) 13 | -------------------------------------------------------------------------------- /web/templates/admin/menu.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}Menu{{ end }} 8 | {{ define "main"}}{{end}} 9 | {{ define "extra" }} 10 | {{template "menu" .}} 11 | {{end}} 12 | {{define "footer"}}{{end}} -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: go-ssb-room 3 | Upstream-Contact: Andre 'Staltz' Medeiros 4 | Source: https://gitlab.com/ssb-ngi-pointer/go-ssb-room-archived 5 | 6 | Files: web/assets/style.css 7 | Copyright: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 8 | License: CC-BY-4.0 -------------------------------------------------------------------------------- /internal/broadcasts/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package broadcasts implements custom typed one-to-n facilities for broadcasting messages/calls to multiple subscribers. 6 | // They loosely follow from luigi.Broadcasts but using concrete types instead of empty interfaces. 7 | package broadcasts 8 | -------------------------------------------------------------------------------- /web/templates/flashes.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "flashes" }} 8 | {{if .Flashes}} 9 | 16 | {{end}} 17 | {{ end }} -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/legacy_client.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const secretStackPlugins = require('./secretstack-legacy') 6 | const before = require('./minimal-before-setup') 7 | const performClientTest = require('./client') 8 | module.exports = { 9 | secretStackPlugins, 10 | before, 11 | after: performClientTest 12 | } 13 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/modern_client.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const secretStackPlugins = require('./secretstack-modern') 6 | const before = require('./minimal-before-setup') 7 | const performClientTest = require('./client') 8 | module.exports = { 9 | secretStackPlugins, 10 | before, 11 | after: performClientTest 12 | } 13 | -------------------------------------------------------------------------------- /docs/files/debian-preremove.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | systemctl stop go-ssb-room 6 | systemctl disable go-ssb-room 7 | # TODO: we might want to have a proper config file so users dont need to tweak this file, then we can also remove and upgrade it properly 8 | # rm /etc/systemd/system/go-ssb-room.service 9 | # systemctl daemon-reload 10 | systemctl reset-failed 11 | -------------------------------------------------------------------------------- /internal/devtools/counterfeiter.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | //go:build tools 6 | // +build tools 7 | 8 | package devtools 9 | 10 | import ( 11 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 12 | ) 13 | 14 | // This file imports packages that are used when running go generate, or used 15 | // during the development process but not otherwise depended on by built code. 16 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/legacy_client_opening_tunnel.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const secretStackPlugins = require('./secretstack-legacy') 6 | const before = require('./minimal-before-setup') 7 | const performOpeningTunnelTest = require('./client-opening-tunnel') 8 | 9 | module.exports = { 10 | secretStackPlugins, 11 | before, 12 | after: performOpeningTunnelTest 13 | } 14 | -------------------------------------------------------------------------------- /web/i18n/dev.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build dev 6 | 7 | package i18n 8 | 9 | import ( 10 | "io/fs" 11 | "os" 12 | "path/filepath" 13 | 14 | "go.mindeco.de/goutils" 15 | ) 16 | 17 | var Defaults fs.FS = os.DirFS(defaultsPath) 18 | 19 | var ( 20 | pkgDir = goutils.MustLocatePackage("github.com/ssbc/go-ssb-room/v2/web/i18n") 21 | defaultsPath = filepath.Join(pkgDir, "defaults") 22 | ) 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/modern_client_opening_tunnel.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const secretStackPlugins = require('./secretstack-modern') // use modern tunnel 6 | const before = require('./minimal-before-setup') 7 | const performOpeningTunnelTest = require('./client-opening-tunnel') 8 | 9 | module.exports = { 10 | secretStackPlugins, 11 | before, 12 | after: performOpeningTunnelTest 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | version: '3' 6 | services: 7 | room: 8 | build: . 9 | command: 'sh start.sh' 10 | env_file: .env 11 | ports: 12 | - "3000:3000" # Proxypass this port through NGINX or Apache as your HTTP landing & dashboard page 13 | - "0.0.0.0:8008:8008" # This is the port SSB clients connect to 14 | volumes: 15 | - ./ssb-go-room-secrets:/ssb-go-room-secrets 16 | -------------------------------------------------------------------------------- /web/styles/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | module.exports = { 6 | purge: ['../templates/**/*.tmpl'], 7 | darkMode: false, 8 | theme: { 9 | extend: { 10 | minHeight: (theme) => ({ 11 | ...theme('spacing'), 12 | }), 13 | } 14 | }, 15 | variants: { 16 | extend: { 17 | backgroundColor: ['odd'], 18 | zIndex: ['hover'], 19 | } 20 | }, 21 | plugins: [], 22 | }; 23 | -------------------------------------------------------------------------------- /web/styles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-go-ssb-room", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "compile-dev": "postcss input.css -o ../assets/style.css", 6 | "compile-prod": "NODE_ENV=production postcss input.css -o ../assets/style.css" 7 | }, 8 | "engines": { 9 | "node": ">=12" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "autoprefixer": "~10.2.4", 14 | "cssnano": "^4.1.10", 15 | "postcss": "~8.2.6", 16 | "postcss-cli": "~8.3.1", 17 | "tailwindcss": "~2.0.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | FROM golang:1.17-alpine 6 | 7 | RUN apk add --no-cache \ 8 | build-base \ 9 | git \ 10 | sqlite \ 11 | sqlite-dev 12 | 13 | RUN mkdir /app 14 | WORKDIR /app 15 | COPY . /app 16 | 17 | RUN cd /app/cmd/server && go build && \ 18 | cd /app/cmd/insert-user && go build 19 | 20 | EXPOSE 8008 21 | EXPOSE 3000 22 | 23 | RUN apk del \ 24 | build-base \ 25 | git 26 | 27 | CMD ./start.sh 28 | -------------------------------------------------------------------------------- /web/i18n/prod.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build !dev 6 | 7 | package i18n 8 | 9 | import ( 10 | "embed" 11 | "io/fs" 12 | "log" 13 | ) 14 | 15 | // Defaults is an embedded filesystem containing translation defaults. 16 | var Defaults fs.FS 17 | 18 | //go:embed defaults/* 19 | var embedDefaults embed.FS 20 | 21 | func init() { 22 | var err error 23 | Defaults, err = fs.Sub(embedDefaults, "defaults") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/templates/error.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{define "title"}}render - Error {{.StatusCode}}{{end}} 8 | {{define "content"}} 9 | 12 |
13 |
14 |

{{.Err}}

15 |

16 | Back 17 |

18 |
19 |
20 | {{end}} -------------------------------------------------------------------------------- /internal/randutil/string.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package randutil 6 | 7 | import "math/rand" 8 | 9 | var alphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 10 | 11 | // String returns a random string of length n, using the alphnum character set (a-z, A-Z, 0-9) 12 | func String(n int) string { 13 | s := make([]rune, n) 14 | 15 | for i := range s { 16 | s[i] = alphabet[rand.Intn(len(alphabet))] 17 | } 18 | 19 | return string(s) 20 | } 21 | -------------------------------------------------------------------------------- /web/embedded_dev.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build dev 6 | 7 | package web 8 | 9 | import ( 10 | "net/http" 11 | "path/filepath" 12 | 13 | "go.mindeco.de/goutils" 14 | ) 15 | 16 | const Production = false 17 | 18 | // absolute path of where this package is located 19 | var pkgDir = goutils.MustLocatePackage("github.com/ssbc/go-ssb-room/v2/web") 20 | 21 | var Templates = http.Dir(filepath.Join(pkgDir, "templates")) 22 | 23 | var Assets = http.Dir(filepath.Join(pkgDir, "assets")) 24 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package sqlite 6 | 7 | import ( 8 | "embed" 9 | 10 | migrate "github.com/rubenv/sql-migrate" 11 | ) 12 | 13 | // migrations is an embedded filesystem containing the sqlite migration files 14 | //go:embed migrations/* 15 | var migrations embed.FS 16 | 17 | // needs https://github.com/rubenv/sql-migrate/pull/189 merged, using my branch until then 18 | var migrationSource = &migrate.EmbedFileSystemMigrationSource{ 19 | FileSystem: migrations, 20 | Root: "migrations", 21 | } 22 | -------------------------------------------------------------------------------- /internal/maybemod/multierror/multierr.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package multierror 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // List contains a list of errors 13 | type List struct{ Errs []error } 14 | 15 | func (el List) Error() string { 16 | var str strings.Builder 17 | 18 | if n := len(el.Errs); n > 0 { 19 | fmt.Fprintf(&str, "multiple errors(%d): ", n) 20 | } 21 | for i, err := range el.Errs { 22 | fmt.Fprintf(&str, "(%d): ", i) 23 | str.WriteString(err.Error() + " - ") 24 | } 25 | 26 | return str.String() 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # vim 6 | .*.sw[a-z] 7 | 8 | # the binaries 9 | cmd/server/server 10 | cmd/insert-user/insert-user 11 | 12 | # testrun contains repos that were use for tests 13 | muxrpc/test/go/testrun 14 | muxrpc/test/nodejs/testrun 15 | web/handlers/testrun 16 | web/handlers/admin/testrun 17 | roomdb/sqlite/testrun 18 | web/i18n/i18ntesting/testrun/ 19 | 20 | # build artifacts from node.js project web/styles 21 | node_modules 22 | 23 | # goreleaser output 24 | dist/ 25 | 26 | # secrets and config 27 | ssb-go-room-secrets/ 28 | .env 29 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/secretstack_testplugin.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | /* 6 | this testing plugin supplies a very simple method to see if the other side is working 7 | */ 8 | module.exports = { 9 | name: 'testing', 10 | version: '1.0.0', 11 | manifest: { 12 | working: 'async' 13 | }, 14 | permissions: { 15 | anonymous: { allow: ['working'] }, 16 | }, 17 | init(ssb) { 18 | return { 19 | working(cb) { 20 | cb(null, true) 21 | } 22 | }; 23 | }, 24 | }; -------------------------------------------------------------------------------- /internal/repo/repo.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package repo 6 | 7 | import "path/filepath" 8 | 9 | type Interface interface { 10 | GetPath(...string) string 11 | } 12 | 13 | var _ Interface = repo{} 14 | 15 | // New creates a new repository value, it opens the keypair and database from basePath if it is already existing 16 | func New(basePath string) Interface { 17 | return repo{basePath: basePath} 18 | } 19 | 20 | type repo struct { 21 | basePath string 22 | } 23 | 24 | func (r repo) GetPath(rel ...string) string { 25 | return filepath.Join(append([]string{r.basePath}, rel...)...) 26 | } 27 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-ssb-rooms-tests", 3 | "version": "1.0.1", 4 | "description": "tests between go and ssb-js", 5 | "main": "sbot_client.js", 6 | "scripts": { 7 | "test": "go test" 8 | }, 9 | "author": "cryptix", 10 | "dependencies": { 11 | "run-parallel": "^1.1.9", 12 | "run-series": "^1.1.9", 13 | "secret-stack": "^6.3.2", 14 | "sodium-native": "^3.2.0", 15 | "ssb-config": "^3.4.5", 16 | "ssb-conn": "^2.1.0", 17 | "ssb-db2": "^1.18.5", 18 | "ssb-keys": "^8.1.0", 19 | "ssb-replicate": "^1.3.2", 20 | "ssb-room": "^1.3.0", 21 | "ssb-room-client": "^0.13.0", 22 | "tap-spec": "^5.0.0", 23 | "tape": "^5.2.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/network/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package network 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | func isConnBrokenErr(err error) bool { 15 | netErr := new(net.OpError) 16 | if errors.As(err, &netErr) { 17 | var sysCallErr = new(os.SyscallError) 18 | if errors.As(netErr.Err, &sysCallErr) { 19 | action := sysCallErr.Unwrap() 20 | if action == syscall.ECONNRESET || action == syscall.EPIPE { 21 | return true 22 | } 23 | } 24 | if netErr.Err.Error() == "use of closed network connection" { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | -------------------------------------------------------------------------------- /roomdb/sqlite/new_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package sqlite 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | _ "modernc.org/sqlite" 14 | 15 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 16 | ) 17 | 18 | // verify the database opens and migrates successfully from zero state 19 | func TestSchema(t *testing.T) { 20 | testRepo := filepath.Join("testrun", t.Name()) 21 | os.RemoveAll(testRepo) 22 | 23 | tr := repo.New(testRepo) 24 | 25 | db, err := Open(tr) 26 | require.NoError(t, err) 27 | 28 | err = db.Close() 29 | require.NoError(t, err) 30 | } 31 | -------------------------------------------------------------------------------- /internal/maybemod/testutils/mergeErrChans.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package testutils 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | // MergeErrorChans is a simple Fan-In for async errors 12 | // TODO: should be replaced with x/sync/errgroup 13 | func MergeErrorChans(cs ...<-chan error) <-chan error { 14 | var wg sync.WaitGroup 15 | out := make(chan error, 1) 16 | 17 | output := func(c <-chan error) { 18 | for a := range c { 19 | out <- a 20 | } 21 | wg.Done() 22 | } 23 | 24 | wg.Add(len(cs)) 25 | for _, c := range cs { 26 | go output(c) 27 | } 28 | 29 | go func() { 30 | wg.Wait() 31 | close(out) 32 | }() 33 | return out 34 | } 35 | -------------------------------------------------------------------------------- /internal/aliases/names_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package aliases 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIsValid(t *testing.T) { 14 | a := assert.New(t) 15 | 16 | cases := []struct { 17 | alias string 18 | valid bool 19 | }{ 20 | {"basic", true}, 21 | {"no spaces", false}, 22 | {"no.dots", false}, 23 | {"#*!(! nope", false}, 24 | 25 | {"NoUpperCase", false}, 26 | 27 | // too long 28 | {"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", false}, 29 | } 30 | 31 | for i, tc := range cases { 32 | yes := IsValid(tc.alias) 33 | a.Equal(tc.valid, yes, "wrong for %d: %s", i, tc.alias) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /roomdb/sqlite/generate_models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | set -e 8 | 9 | # ensure tools are installed 10 | go install github.com/volatiletech/sqlboiler/v4@latest 11 | go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-sqlite3@latest 12 | 13 | # make sure we are in the correct directory 14 | cd "$(dirname $0)" 15 | 16 | # run the migrations (creates testrun/TestSchema/roomdb) 17 | go test -run Schema 18 | 19 | # make sure the sqlite file was created 20 | test -f testrun/TestSchema/roomdb || { 21 | echo 'roomdb file missing' 22 | exit 1 23 | } 24 | 25 | # generate the models package 26 | sqlboiler sqlite3 --wipe --no-tests 27 | 28 | echo "all done. models updated!" 29 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations/03-siwssb-tokens.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | -- 3 | -- SPDX-License-Identifier: CC0-1.0 4 | 5 | -- +migrate Up 6 | -- SIWSSB stands for sign-in with ssb 7 | CREATE TABLE SIWSSB_sessions ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 9 | token TEXT UNIQUE NOT NULL, 10 | member_id INTEGER NOT NULL, 11 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | 13 | FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE 14 | ); 15 | CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token); 16 | CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id); 17 | 18 | -- +migrate Down 19 | DROP INDEX SIWSSB_by_token; 20 | DROP INDEX SIWSSB_by_member; 21 | DROP TABLE SIWSSB_sessions; -------------------------------------------------------------------------------- /web/assets/invite-uri.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | let hasFocus = true; 6 | window.addEventListener('blur', () => { 7 | hasFocus = false; 8 | }); 9 | window.addEventListener('focus', () => { 10 | hasFocus = true; 11 | }); 12 | 13 | const waitingElem = document.getElementById('waiting'); 14 | const anchorElem = document.getElementById('claim-invite-uri'); 15 | anchorElem.onclick = function handleURI(ev) { 16 | ev.preventDefault(); 17 | const ssbUri = anchorElem.href; 18 | const fallbackUrl = anchorElem.dataset.hrefFallback; 19 | waitingElem.classList.remove('hidden'); 20 | setTimeout(function () { 21 | if (hasFocus) window.location.replace(fallbackUrl); 22 | }, 5000); 23 | window.location.replace(ssbUri); 24 | }; 25 | -------------------------------------------------------------------------------- /internal/netwraputil/spoof_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package netwraputil 6 | 7 | import ( 8 | "net" 9 | "testing" 10 | 11 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/keys" 12 | "github.com/ssbc/go-ssb-room/v2/internal/network" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestSpoof(t *testing.T) { 17 | r := require.New(t) 18 | 19 | rc, wc := net.Pipe() 20 | 21 | kp, err := keys.NewKeyPair(nil) 22 | r.NoError(err) 23 | 24 | wrap := SpoofRemoteAddress(kp.Feed.PubKey()) 25 | 26 | wrapped, err := wrap(wc) 27 | r.NoError(err) 28 | 29 | ref, err := network.GetFeedRefFromAddr(wrapped.RemoteAddr()) 30 | r.NoError(err) 31 | r.True(ref.Equal(kp.Feed)) 32 | 33 | wc.Close() 34 | rc.Close() 35 | } 36 | -------------------------------------------------------------------------------- /web/i18n/i18ntesting/i18n_helper_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package i18ntesting 6 | 7 | import ( 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 14 | "github.com/ssbc/go-ssb-room/v2/roomdb/mockdb" 15 | "github.com/ssbc/go-ssb-room/v2/web/i18n" 16 | ) 17 | 18 | func TestListLanguages(t *testing.T) { 19 | configDB := new(mockdb.FakeRoomConfig) 20 | configDB.GetDefaultLanguageReturns("en", nil) 21 | r := repo.New(filepath.Join("testrun", t.Name())) 22 | a := assert.New(t) 23 | helper, err := i18n.New(r, configDB) 24 | a.NoError(err) 25 | t.Log(helper) 26 | translation := helper.ChooseTranslation("en") 27 | a.Equal(translation, "English") 28 | } 29 | -------------------------------------------------------------------------------- /docs/files/example-systemd.service: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | [Unit] 6 | Description="The showcase v2 room" 7 | After=network.target 8 | # Uncomment these if you are using it with nginx 9 | # After=nginx.target 10 | # Wants=nginx.target 11 | 12 | [Service] 13 | # you need to change your -https-domain here. replace 'my-example-room.somewhere' with what you are using. 14 | # if you are using a different http configuration, you might also need to change value behind -lishttp. 15 | ExecStart=/usr/local/bin/go-ssb-room -repo /var/lib/go-ssb-room -lishttp localhost:8899 -https-domain my-example-room.somewhere 16 | WorkingDirectory=/var/lib/go-ssb-room 17 | Restart=always 18 | SyslogIdentifier=gossbroom 19 | User=go-ssb-room 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /web/handlers/language_template.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package handlers 6 | 7 | import "html/template" 8 | 9 | type changeLanguageTemplateData struct { 10 | PostRoute string 11 | CSRFElement template.HTML 12 | LangTag string 13 | RedirectPage string 14 | Translation string 15 | ClassList string 16 | } 17 | 18 | var changeLanguageTemplate = template.Must(template.New("changeLanguageForm").Parse(` 19 |
23 | {{ .CSRFElement }} 24 | 25 | 26 | 31 |
`)) 32 | -------------------------------------------------------------------------------- /roomdb/role_string.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Code generated by "stringer -type=Role"; DO NOT EDIT. 6 | 7 | package roomdb 8 | 9 | import "strconv" 10 | 11 | func _() { 12 | // An "invalid array index" compiler error signifies that the constant values have changed. 13 | // Re-run the stringer command to generate them again. 14 | var x [1]struct{} 15 | _ = x[RoleUnknown-0] 16 | _ = x[RoleMember-1] 17 | _ = x[RoleModerator-2] 18 | _ = x[RoleAdmin-3] 19 | } 20 | 21 | const _Role_name = "RoleUnknownRoleMemberRoleModeratorRoleAdmin" 22 | 23 | var _Role_index = [...]uint8{0, 11, 21, 34, 43} 24 | 25 | func (i Role) String() string { 26 | if i >= Role(len(_Role_index)-1) { 27 | return "Role(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | return _Role_name[_Role_index[i]:_Role_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /web/templates/notice/show.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{.Title}}{{ end }} 8 | {{ define "content" }} 9 | 10 | {{ template "flashes" . }} 11 | 12 |

{{.Title}}

15 | 16 |
17 | {{.Content}} 18 |
19 | 20 |
21 | {{if and is_logged_in member_is_elevated }} 22 | {{i18n "NoticeEditTitle"}} 27 | {{end}} 28 | {{end}} 29 | -------------------------------------------------------------------------------- /web/templates/landing/index.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{.Title}}{{ end }} 8 | {{ define "content" }} 9 |

{{.Title}}

12 | 13 |
14 | {{.Content}} 15 |
16 | 17 |
18 | {{if and is_logged_in member_is_elevated }} 19 | {{i18n "NoticeEditTitle"}} 24 | {{end}} 25 | {{end}} 26 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/modern_aliases.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const secretStackPlugins = require('./secretstack-modern') 6 | const before = require('./minimal-before-setup') 7 | 8 | module.exports = { 9 | secretStackPlugins, 10 | before, 11 | after: (t, sbot, rpc, exit) => { 12 | 13 | // give ssb-conn a moment to settle 14 | setTimeout(() => { 15 | 16 | sbot.roomClient.registerAlias(rpc.id, "alice", (err, ret) => { 17 | t.error(err, 'registerAlias') 18 | t.ok(ret) 19 | t.equals(typeof ret, 'string') 20 | t.ok(new URL(ret)) 21 | 22 | sbot.roomClient.revokeAlias(rpc.id, "alice", (err, ret) => { 23 | t.error(err, 'revokeAlias') 24 | t.comment(`revokeAlias value: ${ret}`) 25 | exit() 26 | }) 27 | }) 28 | 29 | }, 1000) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/members/testing.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package members 6 | 7 | import ( 8 | "context" 9 | "net/http" 10 | 11 | "github.com/ssbc/go-ssb-room/v2/roomdb" 12 | ) 13 | 14 | // MiddlewareForTests gives us a way to inject _test members_. It should not be used in production. 15 | // This is part of testing.go because we need to use roomMemberContextKey, which shouldn't be exported either. 16 | // TODO: could be protected with an extra build tag. 17 | // (Sadly +build test does not exist https://github.com/golang/go/issues/21360 ) 18 | func MiddlewareForTests(m *roomdb.Member) func(http.Handler) http.Handler { 19 | return func(next http.Handler) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 21 | ctx := context.WithValue(req.Context(), roomMemberContextKey, m) 22 | next.ServeHTTP(w, req.WithContext(ctx)) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /roomdb/privacymode_string.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Code generated by "stringer -type=PrivacyMode"; DO NOT EDIT. 6 | 7 | package roomdb 8 | 9 | import "strconv" 10 | 11 | func _() { 12 | // An "invalid array index" compiler error signifies that the constant values have changed. 13 | // Re-run the stringer command to generate them again. 14 | var x [1]struct{} 15 | _ = x[ModeUnknown-0] 16 | _ = x[ModeOpen-1] 17 | _ = x[ModeCommunity-2] 18 | _ = x[ModeRestricted-3] 19 | } 20 | 21 | const _PrivacyMode_name = "ModeUnknownModeOpenModeCommunityModeRestricted" 22 | 23 | var _PrivacyMode_index = [...]uint8{0, 11, 19, 32, 46} 24 | 25 | func (i PrivacyMode) String() string { 26 | if i >= PrivacyMode(len(_PrivacyMode_index)-1) { 27 | return "PrivacyMode(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | return _PrivacyMode_name[_PrivacyMode_index[i]:_PrivacyMode_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /muxrpc/handlers/whoami/whoami.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package whoami 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/ssbc/go-muxrpc/v2/typemux" 11 | 12 | "github.com/ssbc/go-muxrpc/v2" 13 | kitlog "go.mindeco.de/log" 14 | "go.mindeco.de/log/level" 15 | 16 | refs "github.com/ssbc/go-ssb-refs" 17 | ) 18 | 19 | var ( 20 | method = muxrpc.Method{"whoami"} 21 | ) 22 | 23 | func checkAndLog(log kitlog.Logger, err error) { 24 | if err != nil { 25 | level.Warn(log).Log("event", "faild to write panic file", "err", err) 26 | } 27 | } 28 | 29 | func New(id refs.FeedRef) typemux.AsyncHandler { 30 | return handler{id: id} 31 | } 32 | 33 | type handler struct { 34 | id refs.FeedRef 35 | } 36 | 37 | func (h handler) HandleAsync(ctx context.Context, req *muxrpc.Request) (interface{}, error) { 38 | type ret struct { 39 | ID string `json:"id"` 40 | } 41 | 42 | return ret{h.id.String()}, nil 43 | } 44 | -------------------------------------------------------------------------------- /web/embedded_prod.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // +build !dev 6 | 7 | package web 8 | 9 | import ( 10 | "embed" 11 | "io/fs" 12 | "log" 13 | "net/http" 14 | ) 15 | 16 | // Production can be used to determain different aspects at compile time (like hot template reloading) 17 | const Production = true 18 | 19 | var ( 20 | Assets http.FileSystem 21 | Templates http.FileSystem 22 | ) 23 | 24 | // correct the paths by stripping their prefixes 25 | func init() { 26 | var err error 27 | 28 | prefixedAssets, err := fs.Sub(embedAssets, "assets") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | Assets = http.FS(prefixedAssets) 33 | 34 | prefixedTemplates, err := fs.Sub(embedTemplates, "templates") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | Templates = http.FS(prefixedTemplates) 39 | } 40 | 41 | //go:embed templates/* 42 | var embedTemplates embed.FS 43 | 44 | //go:embed assets/* 45 | var embedAssets embed.FS 46 | -------------------------------------------------------------------------------- /internal/maybemod/multicloser/multicloser.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package multicloser 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "sync" 11 | 12 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/multierror" 13 | ) 14 | 15 | type Closer struct { 16 | cs []io.Closer 17 | l sync.Mutex 18 | } 19 | 20 | func (mc *Closer) Add(c io.Closer) { 21 | mc.l.Lock() 22 | defer mc.l.Unlock() 23 | 24 | mc.cs = append(mc.cs, c) 25 | } 26 | 27 | var _ io.Closer = (*Closer)(nil) 28 | 29 | func (mc *Closer) Close() error { 30 | mc.l.Lock() 31 | defer mc.l.Unlock() 32 | 33 | var ( 34 | hasErrs bool 35 | errs []error 36 | ) 37 | 38 | for i, c := range mc.cs { 39 | if cerr := c.Close(); cerr != nil { 40 | cerr = fmt.Errorf("Closer: c%d failed: %w", i, cerr) 41 | errs = append(errs, cerr) 42 | hasErrs = true 43 | } 44 | } 45 | 46 | if !hasErrs { 47 | return nil 48 | } 49 | 50 | return multierror.List{Errs: errs} 51 | } 52 | -------------------------------------------------------------------------------- /roomdb/sqlite/sqlboiler.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | [sqlite3] 6 | # go test in the roomdb/sqlite package will create this 7 | dbname = "testrun/TestSchema/roomdb" 8 | blacklist = ["gorp_migrations"] 9 | 10 | # marshal pub_key strings ala @asdjjasd as feed references. 11 | [[types]] 12 | [types.match] 13 | type = "string" 14 | #tables = ['fallback_auth'] 15 | name = "pub_key" 16 | nullable = false 17 | 18 | [types.replace] 19 | type = "roomdb.DBFeedRef" 20 | 21 | [types.imports] 22 | third_party = ['"github.com/ssbc/go-ssb-room/v2/roomdb"'] 23 | 24 | # convert from database-stored integers to the type roomdb.RoomConfig 25 | [[types]] 26 | [types.match] 27 | name = "privacyMode" 28 | tables = ['config'] 29 | type = "int64" 30 | nullable = false 31 | 32 | [types.replace] 33 | type = "roomdb.PrivacyMode" 34 | 35 | [types.imports] 36 | third_party = ['"github.com/ssbc/go-ssb-room/v2/roomdb"'] 37 | -------------------------------------------------------------------------------- /roomdb/sqlite/models/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.14.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | SIWSSBSessions string 8 | Aliases string 9 | Config string 10 | DeniedKeys string 11 | FallbackPasswords string 12 | FallbackResetTokens string 13 | Invites string 14 | Members string 15 | Notices string 16 | PinNotices string 17 | Pins string 18 | }{ 19 | SIWSSBSessions: "SIWSSB_sessions", 20 | Aliases: "aliases", 21 | Config: "config", 22 | DeniedKeys: "denied_keys", 23 | FallbackPasswords: "fallback_passwords", 24 | FallbackResetTokens: "fallback_reset_tokens", 25 | Invites: "invites", 26 | Members: "members", 27 | Notices: "notices", 28 | PinNotices: "pin_notices", 29 | Pins: "pins", 30 | } 31 | -------------------------------------------------------------------------------- /web/templates/admin/invite-created.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminInviteCreatedTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | 12 | 13 | 14 | {{i18n "AdminInviteCreatedTitle"}}
{{i18n "AdminInviteCreatedInstruct"}}
18 | 19 | {{.FacadeURL}} 24 |
25 | {{end}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 SSB NGI-Pointer Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /web/assets/alias-uri.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | let hasFocus = true; 6 | window.addEventListener('blur', () => { 7 | hasFocus = false; 8 | }); 9 | window.addEventListener('focus', () => { 10 | hasFocus = true; 11 | }); 12 | 13 | const waitingElem = document.getElementById('waiting'); 14 | const failureElem = document.getElementById('failure'); 15 | const anchorElem = document.getElementById('alias-uri'); 16 | 17 | // Autoredirect to the ssb uri ASAP 18 | setTimeout(() => { 19 | const ssbUri = anchorElem.href; 20 | window.location.replace(ssbUri); 21 | }, 100); 22 | 23 | // Redirect to ssb uri or show failure state 24 | anchorElem.onclick = function handleURI(ev) { 25 | ev.preventDefault(); 26 | const ssbUri = anchorElem.href; 27 | waitingElem.classList.remove('hidden'); 28 | setTimeout(function () { 29 | if (hasFocus) { 30 | waitingElem.classList.add('hidden'); 31 | failureElem.classList.remove('hidden'); 32 | } 33 | }, 5000); 34 | window.location.replace(ssbUri); 35 | }; 36 | -------------------------------------------------------------------------------- /internal/network/msaddr_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package network 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "strings" 11 | "testing" 12 | 13 | refs "github.com/ssbc/go-ssb-refs" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestMultiserverAddress(t *testing.T) { 18 | a := assert.New(t) 19 | 20 | var sed ServerEndpointDetails 21 | sed.Domain = "the.ho.st" 22 | sed.ListenAddressMUXRPC = ":8008" 23 | 24 | roomID, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte("ohai"), 8), refs.RefAlgoFeedSSB1) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | sed.RoomID = roomID 29 | 30 | gotMultiAddr := sed.MultiserverAddress() 31 | 32 | a.Equal("net:the.ho.st:8008~shs:b2hhaW9oYWlvaGFpb2hhaW9oYWlvaGFpb2hhaW9oYWk=", gotMultiAddr) 33 | a.True(strings.HasPrefix(gotMultiAddr, "net:the.ho.st:8008~shs:"), "not for the test host? %s", gotMultiAddr) 34 | a.True(strings.HasSuffix(gotMultiAddr, base64.StdEncoding.EncodeToString(sed.RoomID.PubKey())), "public key missing? %s", gotMultiAddr) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /roomdb/sqlite/models/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.14.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "regexp" 8 | 9 | "github.com/volatiletech/sqlboiler/v4/drivers" 10 | "github.com/volatiletech/sqlboiler/v4/queries" 11 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 12 | ) 13 | 14 | var dialect = drivers.Dialect{ 15 | LQ: 0x22, 16 | RQ: 0x22, 17 | 18 | UseIndexPlaceholders: false, 19 | UseLastInsertID: false, 20 | UseSchema: false, 21 | UseDefaultKeyword: true, 22 | UseAutoColumns: false, 23 | UseTopClause: false, 24 | UseOutputClause: false, 25 | UseCaseWhenExistsClause: false, 26 | } 27 | 28 | // This is a dummy variable to prevent unused regexp import error 29 | var _ = ®exp.Regexp{} 30 | 31 | // NewQuery initializes a new Query using the passed in QueryMods 32 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 33 | q := &queries.Query{} 34 | queries.SetDialect(q, &dialect) 35 | qm.Apply(q, mods...) 36 | 37 | return q 38 | } 39 | -------------------------------------------------------------------------------- /web/templates/admin/members-show-password-reset-token.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | 12 | 13 | 14 | {{i18n "AdminMemberPasswordResetLinkCreatedTitle"}}
{{i18n "AdminMemberPasswordResetLinkCreatedInstruct"}}
18 | 19 | {{.ResetLinkURL}} 24 |
25 | {{end}} -------------------------------------------------------------------------------- /internal/aliases/names.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package aliases 6 | 7 | // IsValid decides whether an alias is okay for use or not. 8 | // The room spec defines it as _labels valid under RFC 1035_ ( https://ssbc.github.io/rooms2/#alias-string ) 9 | // but that can be mostly any string since DNS is a 8bit binary protocol, 10 | // as long as it's shorter then 63 charachters. 11 | // 12 | // Right now it's pretty basic set of characters (a-z, 0-9). 13 | // In theory we could be more liberal but there is a bunch of stuff to figure out, 14 | // like homograph attacks (https://en.wikipedia.org/wiki/IDN_homograph_attack), 15 | // if we would decide to allow full utf8 unicode. 16 | func IsValid(alias string) bool { 17 | if len(alias) > 63 { 18 | return false 19 | } 20 | 21 | var valid = true 22 | for _, char := range alias { 23 | if char >= '0' && char <= '9' { // is an ASCII number 24 | continue 25 | } 26 | 27 | if char >= 'a' && char <= 'z' { // is an ASCII char between a and z 28 | continue 29 | } 30 | 31 | valid = false 32 | break 33 | } 34 | return valid 35 | } 36 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and 6 | successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For more information, please refer to 11 | -------------------------------------------------------------------------------- /internal/netwraputil/spoof.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package netwraputil 6 | 7 | import ( 8 | "errors" 9 | "net" 10 | 11 | "github.com/ssbc/go-netwrap" 12 | "github.com/ssbc/go-secretstream" 13 | ) 14 | 15 | // SpoofRemoteAddress wraps the connection with the passed reference 16 | // as if it was a secret-handshake connection 17 | // warning: should only be used where auth is established otherwise, 18 | // like for testing or local client access over unixsock 19 | func SpoofRemoteAddress(pubKey []byte) netwrap.ConnWrapper { 20 | if len(pubKey) != 32 { 21 | return func(_ net.Conn) (net.Conn, error) { 22 | return nil, errors.New("invalid public key length") 23 | } 24 | } 25 | var spoofedAddr secretstream.Addr 26 | spoofedAddr.PubKey = pubKey 27 | return func(c net.Conn) (net.Conn, error) { 28 | sc := SpoofedConn{ 29 | Conn: c, 30 | spoofedRemote: netwrap.WrapAddr(c.RemoteAddr(), spoofedAddr), 31 | } 32 | return sc, nil 33 | } 34 | } 35 | 36 | type SpoofedConn struct { 37 | net.Conn 38 | 39 | spoofedRemote net.Addr 40 | } 41 | 42 | func (sc SpoofedConn) RemoteAddr() net.Addr { 43 | return sc.spoofedRemote 44 | } 45 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/legacy_server.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const pull = require('pull-stream') 6 | const path = require("path") 7 | const scriptname = path.basename(__filename) 8 | 9 | let connections = 0 10 | 11 | module.exports = { 12 | secretStackPlugins: ['ssb-conn', 'ssb-room/tunnel/server'], 13 | 14 | before: (t, sbot, ready) => { 15 | pull( 16 | sbot.conn.hub().listen(), 17 | pull.drain((p) => { 18 | t.comment(`[legacy-server.js] peer change ${p.type}: ${p.key}`) 19 | }) 20 | ) 21 | setTimeout(ready, 1000) 22 | }, 23 | 24 | after: (t, sbot, client, exit) => { 25 | function comment (msg) { 26 | t.comment(`[${scriptname}] ${msg}`) 27 | } 28 | // this runs twice (for each connection) 29 | connections++ 30 | comment(`new connection: ${client.id}`) 31 | comment(`total connections: ${connections}`) 32 | 33 | if (connections == 2) { 34 | t.comment('2nd connection received. exiting in 10 seconds') 35 | setTimeout(exit, 10000) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/handlers/basic_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package handlers 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/ssbc/go-ssb-room/v2/web/router" 15 | "github.com/ssbc/go-ssb-room/v2/web/webassert" 16 | ) 17 | 18 | func TestIndex(t *testing.T) { 19 | ts := setup(t) 20 | 21 | a := assert.New(t) 22 | 23 | url := ts.URLTo(router.CompleteIndex) 24 | 25 | html, resp := ts.Client.GetHTML(url) 26 | a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") 27 | webassert.Localized(t, html, []webassert.LocalizedElement{ 28 | {"h1", "Default Notice Title"}, 29 | {"title", "Default Notice Title"}, 30 | }) 31 | 32 | content := html.Find("p").Text() 33 | a.Equal("Default Notice Content", content) 34 | } 35 | 36 | func TestNotFound(t *testing.T) { 37 | ts := setup(t) 38 | 39 | a := assert.New(t) 40 | 41 | url404, err := url.Parse("/some/random/ASDKLANZXC") 42 | a.NoError(err) 43 | 44 | html, resp := ts.Client.GetHTML(url404) 45 | a.Equal(http.StatusNotFound, resp.Code, "wrong HTTP status code") 46 | found := html.Find("h1").Text() 47 | a.Equal("Error #404 - Not Found", found) 48 | } 49 | -------------------------------------------------------------------------------- /web/router/auth.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package router 6 | 7 | import "github.com/gorilla/mux" 8 | 9 | // constant names for the named routes 10 | const ( 11 | AuthLogin = "auth:login" 12 | AuthLogout = "auth:logout" 13 | 14 | AuthFallbackLogin = "auth:fallback:login" 15 | AuthFallbackFinalize = "auth:fallback:finalize" 16 | 17 | AuthWithSSBLogin = "auth:withssb:login" 18 | AuthWithSSBServerEvents = "auth:withssb:sse" 19 | AuthWithSSBFinalize = "auth:withssb:finalize" 20 | ) 21 | 22 | // Auth constructs a mux.Router containing the routes for sign-in and -out 23 | func Auth(m *mux.Router) *mux.Router { 24 | if m == nil { 25 | m = mux.NewRouter() 26 | } 27 | 28 | m.Path("/login").Methods("GET").Name(AuthLogin) 29 | m.Path("/logout").Methods("GET").Name(AuthLogout) 30 | 31 | m.Path("/fallback/login").Methods("GET").Name(AuthFallbackLogin) 32 | m.Path("/fallback/finalize").Methods("POST").Name(AuthFallbackFinalize) 33 | 34 | m.Path("/withssb/login").Methods("GET").Name(AuthWithSSBLogin) 35 | m.Path("/withssb/events").Methods("GET").Name(AuthWithSSBServerEvents) 36 | m.Path("/withssb/finalize").Methods("GET").Name(AuthWithSSBFinalize) 37 | 38 | return m 39 | } 40 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations/03-config.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | -- 3 | -- SPDX-License-Identifier: CC0-1.0 4 | 5 | -- +migrate Up 6 | -- the configuration settings for this room, currently privacy mode settings and the default translation for the room 7 | CREATE TABLE config ( 8 | id integer NOT NULL PRIMARY KEY, 9 | privacyMode integer NOT NULL, -- open, community, restricted 10 | defaultLanguage TEXT NOT NULL, -- a language tag, e.g. en, sv, de 11 | use_subdomain_for_aliases boolean NOT NULL, -- flag to toggle using subdomains (rather than alias routes) for aliases 12 | 13 | CHECK (id == 0) -- should only ever store one row 14 | ); 15 | 16 | -- the config table will only ever contain one row: the rooms current settings 17 | -- we update that row whenever the config changes. 18 | -- to have something to update, we insert the first and only row at id 0 19 | INSERT INTO config (id, privacyMode, defaultLanguage, use_subdomain_for_aliases) VALUES ( 20 | 0, -- the constant id we will query 21 | 2, -- community is the default mode unless overridden 22 | "en", -- english is the default language for all installs 23 | 1 -- use subdomain for aliases 24 | ); 25 | 26 | -- +migrate Down 27 | DROP TABLE config; 28 | -------------------------------------------------------------------------------- /roomdb/sqlite/roomconfig_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package sqlite 6 | 7 | import ( 8 | "context" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/ssbc/go-ssb-room/v2/roomdb" 14 | 15 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestRoomConfig(t *testing.T) { 20 | r := require.New(t) 21 | ctx := context.Background() 22 | 23 | testRepo := filepath.Join("testrun", t.Name()) 24 | os.RemoveAll(testRepo) 25 | 26 | tr := repo.New(testRepo) 27 | 28 | db, err := Open(tr) 29 | r.NoError(err) 30 | 31 | // make sure we have the expected default 32 | pm, err := db.Config.GetPrivacyMode(ctx) 33 | r.NoError(err) 34 | r.Equal(pm, roomdb.ModeCommunity, "privacy mode was unknown: %s", pm) 35 | 36 | // test setting a valid privacy mode 37 | err = db.Config.SetPrivacyMode(ctx, roomdb.ModeRestricted) 38 | r.NoError(err) 39 | 40 | // make sure the mode was set correctly by getting it 41 | pm, err = db.Config.GetPrivacyMode(ctx) 42 | r.NoError(err) 43 | r.Equal(pm, roomdb.ModeRestricted, "privacy mode was unknown") 44 | 45 | // test setting an invalid privacy mode 46 | err = db.Config.SetPrivacyMode(ctx, 1337) 47 | r.Error(err) 48 | } 49 | -------------------------------------------------------------------------------- /web/templates/invite/insert-id.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }} 8 | {{ define "content" }} 9 |
10 | {{i18n "InviteInsertWelcome"}} 11 | 12 |
18 | {{.csrfField}} 19 | 20 | 21 | 26 | 27 | 31 |
32 |
33 | {{ end }} 34 | -------------------------------------------------------------------------------- /web/templates/notice/list.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "NoticeList"}}{{ end }} 8 | {{ define "content" }} 9 |
10 |

{{i18n "NoticeList"}}

13 | 14 |

{{i18n "NoticeListWelcome"}}

15 | 16 | {{ template "flashes" . }} 17 | 18 | {{range .AllNotices}} 19 |
20 |

{{i18n .Name.String}}

21 | 22 | {{range .Notices}} 23 | {{.Language}} 27 | {{end}} 28 | 29 | {{if and is_logged_in member_is_elevated }} 30 | {{i18n "NoticeAddTranslation"}} 34 | {{end}} 35 |
36 | {{end}} 37 |
38 | {{end}} 39 | -------------------------------------------------------------------------------- /internal/signinwithssb/simple_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package signinwithssb 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | refs "github.com/ssbc/go-ssb-refs" 15 | ) 16 | 17 | func TestPayloadString(t *testing.T) { 18 | 19 | server, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{1}, 32), refs.RefAlgoFeedSSB1) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | client, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{2}, 32), refs.RefAlgoFeedSSB1) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | 29 | var req ClientPayload 30 | 31 | req.ServerID = server 32 | req.ClientID = client 33 | 34 | req.ServerChallenge = "fooo" 35 | req.ClientChallenge = "barr" 36 | 37 | want := "=http-auth-sign-in:@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=.ed25519:@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=.ed25519:fooo:barr" 38 | 39 | got := req.createMessage() 40 | assert.Equal(t, want, string(got)) 41 | } 42 | 43 | func TestGenerateAndDecode(t *testing.T) { 44 | r := require.New(t) 45 | 46 | b, err := DecodeChallengeString(GenerateChallenge()) 47 | r.NoError(err) 48 | r.Len(b, challengeLength) 49 | 50 | b, err = DecodeChallengeString("toshort") 51 | r.Error(err) 52 | r.Nil(b) 53 | } 54 | -------------------------------------------------------------------------------- /web/templates/invite/facade.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }} 8 | {{ define "content" }} 9 |
10 |

{{i18nWithData "InviteFacadeWelcome" "RoomTitle" .RoomTitle}}

11 |

{{i18n "InviteFacadeInstruct"}}

12 | 13 | {{i18n "InviteFacadeJoin"}} 19 | 20 | 21 | 22 |
23 | 24 | {{i18n "InviteFacadeInstructQR"}} 25 | 26 | QR-Code to pass the challenge to an App 34 | 35 | 36 |
37 | {{ end }} -------------------------------------------------------------------------------- /roomsrv/manifest.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package roomsrv 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | 12 | "github.com/ssbc/go-muxrpc/v2" 13 | ) 14 | 15 | type manifestHandler string 16 | 17 | func (h manifestHandler) HandleAsync(ctx context.Context, req *muxrpc.Request) (interface{}, error) { 18 | return json.RawMessage(h), nil 19 | } 20 | 21 | func init() { 22 | if !json.Valid([]byte(manifest)) { 23 | manifestMap := make(map[string]interface{}) 24 | err := json.Unmarshal([]byte(manifest), &manifestMap) 25 | fmt.Println(err) 26 | panic("manifest blob is broken json") 27 | } 28 | } 29 | 30 | // this is a very simple hardcoded manifest.json dump which oasis' ssb-client expects to do it's magic. 31 | const manifest manifestHandler = ` 32 | { 33 | "manifest": "sync", 34 | 35 | "whoami":"async", 36 | 37 | "gossip": { 38 | "ping": "duplex" 39 | }, 40 | 41 | "room": { 42 | "registerAlias": "async", 43 | "revokeAlias": "async", 44 | "listAliases": "async", 45 | 46 | "connect": "duplex", 47 | "attendants": "source", 48 | "members": "source", 49 | "metadata": "async", 50 | "ping": "sync" 51 | }, 52 | 53 | "tunnel": { 54 | "announce": "sync", 55 | "leave": "sync", 56 | "connect": "duplex", 57 | "endpoints": "source", 58 | "isRoom": "async", 59 | "ping": "sync" 60 | } 61 | }` 62 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: Go 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Node (for interop testing) 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 14.x 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: 1.20.5 29 | 30 | - name: Get dependencies 31 | run: go get -v -t -d ./... 32 | 33 | - name: Build smoke test 34 | run: go build ./cmd/... 35 | 36 | - name: Build dev smoke test 37 | run: go build -tags dev ./cmd/... 38 | 39 | - name: install node ssb-stack 40 | run: | 41 | pushd muxrpc/test/nodejs 42 | npm ci 43 | popd 44 | 45 | - name: All the Test 46 | run: go test ./... 47 | 48 | - name: update style.css 49 | run: | 50 | pushd web/styles 51 | npm ci 52 | npm run compile-prod 53 | popd 54 | 55 | - name: push updated style.css 56 | uses: stefanzweifel/git-auto-commit-action@v4 57 | with: 58 | commit_message: update production style.css 59 | file_pattern: web/assets/style.css 60 | -------------------------------------------------------------------------------- /web/templates/auth/fallback_sign_in.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AuthTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | {{i18n "AuthFallbackWelcome"}} 11 | 12 | {{ template "flashes" . }} 13 | 14 |
20 | {{ .csrfField }} 21 |
22 | 23 | 25 | 26 | 28 | 30 |
31 |
32 |
33 | {{end}} -------------------------------------------------------------------------------- /web/templates/invite/consumed.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "InviteConsumedTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | 12 | 13 | 14 | {{i18n "InviteConsumedTitle"}}
{{i18n "InviteConsumedWelcome"}}
18 | 19 | {{.MultiserverAddress}} 22 | 23 | {{i18n "InviteConsumedSetPassword" }}
26 | 27 | {{i18n "InviteConsumedSetPasswordButton" }} 33 |
34 | {{end}} 35 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Architecture 8 | 9 | ## Invite flow 10 | 11 | This implementation of Rooms 2.0 is compliant with the [Rooms 2.0 12 | specification](https://github.com/ssbc/rooms2), but we add a few additional features 13 | and pages in order to improve user experience when their SSB app does not support [SSB 14 | URIs](https://github.com/ssbc/ssb-uri-spec). 15 | 16 | A summary can be seen in the following chart: 17 | 18 | ![Chart](./images/invites-chart.png) 19 | 20 | When the browser and operating system detects no support for opening SSB URIs, we redirect to a 21 | fallback page which presents the user with two broad options: (1) install an SSB app that 22 | supports SSB URIs, (2) link to another page where the user can manually input the user's SSB ID 23 | in a form. 24 | 25 | ## Sign-in flow 26 | 27 | This implementation is compliant with [SSB HTTP 28 | Authentication](https://github.com/ssbc/ssb-http-auth-spec), but we add a few 29 | additional features and pages in order to improve user experience. For instance, besides 30 | conventional SSB HTTP Auth, we also render a QR code to sign-in with a remote SSB app (an SSB 31 | identity not on the device that has the browser open). We also support sign-in with 32 | username/password, what we call "fallback authentication". 33 | 34 | A summary can be seen in the following chart: 35 | 36 | ![Chart](./images/login-chart.png) 37 | -------------------------------------------------------------------------------- /web/templates/admin/members-remove-confirm.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminMembersRemoveConfirmTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | {{i18n "AdminMembersRemoveConfirmWelcome"}} 15 | 16 |
{{.Entry.PubKey.String}}
20 | 21 |
22 | {{ .csrfField }} 23 | 24 |
25 | {{i18n "GenericGoBack"}} 29 | 30 | 34 |
35 |
36 |
37 | {{end}} 38 | -------------------------------------------------------------------------------- /web/templates/admin/aliases-revoke-confirm.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminAliasesRevokeConfirmTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | {{i18n "AdminAliasesRevokeConfirmWelcome"}} 15 | 16 |
{{.Entry.Feed.String}}
20 | 21 |
22 | {{ .csrfField }} 23 | 24 |
25 | {{i18n "GenericGoBack"}} 29 | 30 | 34 |
35 |
36 |
37 | {{end}} 38 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/template.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | /* 6 | this is a tempalte for a script to be used in the go<>js tests. 7 | 8 | all the setup of the peers is done in sbot_client and sbot_server js. 9 | 10 | warning: only log to stderr (console.warn) 11 | DONT log to stdout (console.log) as this is connected to the go test process for initialization 12 | 13 | TODO: pass the tape instance into the module, so that t.error and it's other helpers can be used. 14 | proably by turning the exported object into an init function which returns the {before, after} object. 15 | */ 16 | 17 | // const pull = require('pull-stream') 18 | 19 | module.exports = { 20 | secretStackPlugins: ['ssb-blobs', 'ssb-what-ever-you-need'], 21 | 22 | // t is the tape instance for assertions 23 | // sbot is the local sbot api 24 | // ready is a function to signal that preperation is done 25 | before: (t, sbot, ready) => { 26 | console.warn('before connect...') 27 | setTimeout(ready, 1000) 28 | }, 29 | 30 | // t and sbot are same as above 31 | // clientRpc is the muxrpc client to the other remote (i.e a rpc handle for the room the client is connected to) 32 | // exit() is a function that needs to be called to halt the process and exit (it also calls t.end()) 33 | after: (t, sbot, clientRpc, exit) => { 34 | console.warn('after connect...') 35 | 36 | setTimeout(exit, 5000) 37 | } 38 | } -------------------------------------------------------------------------------- /roomdb/sqlite/models/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.14.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/friendsofgo/errors" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /muxrpc/handlers/tunnel/server/members.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package server 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | 12 | "github.com/ssbc/go-muxrpc/v2" 13 | refs "github.com/ssbc/go-ssb-refs" 14 | "github.com/ssbc/go-ssb-room/v2/internal/network" 15 | "github.com/ssbc/go-ssb-room/v2/roomdb" 16 | ) 17 | 18 | type Member struct { 19 | ID refs.FeedRef `json:"id"` 20 | } 21 | 22 | func (h *Handler) members(ctx context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink) error { 23 | peer, err := network.GetFeedRefFromAddr(req.RemoteAddr()) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | pm, err := h.config.GetPrivacyMode(ctx) 29 | if err != nil { 30 | return fmt.Errorf("running with unknown privacy mode: %w", err) 31 | } 32 | 33 | if pm == roomdb.ModeCommunity || pm == roomdb.ModeRestricted { 34 | _, err := h.membersdb.GetByFeed(ctx, peer) 35 | if err != nil { 36 | return fmt.Errorf("external users are not allowed to list members: %w", err) 37 | } 38 | } 39 | 40 | members, err := h.membersdb.List(ctx) 41 | if err != nil { 42 | return fmt.Errorf("error listing members: %w", err) 43 | } 44 | 45 | snk.SetEncoding(muxrpc.TypeJSON) 46 | 47 | for _, member := range members { 48 | if err = json.NewEncoder(snk).Encode([]Member{ 49 | { 50 | ID: member.PubKey, 51 | }, 52 | }); err != nil { 53 | return fmt.Errorf("encoder error: %w", err) 54 | } 55 | } 56 | 57 | return snk.Close() 58 | } 59 | -------------------------------------------------------------------------------- /web/templates/alias.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{.Alias.Name}}{{ end }} 8 | {{ define "content" }} 9 |
10 |

{{.Alias.Name}} is a member of this SSB room server.

11 |

{{i18n "AliasResolutionInstruct"}}

12 | 13 | {{i18n "AliasResolutionConnect"}} {{.Alias.Name}} 18 | 19 | 20 | 21 | 30 | 31 |
32 | 33 |
34 | {{end}} -------------------------------------------------------------------------------- /internal/maybemuxrpc/plugin.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package maybemuxrpc 6 | 7 | import ( 8 | "net" 9 | "sync" 10 | 11 | "github.com/ssbc/go-muxrpc/v2" 12 | ) 13 | 14 | type Authorizer interface { 15 | Authorize(net.Conn) bool 16 | } 17 | 18 | type Plugin interface { 19 | // Name returns the name and version of the plugin. 20 | // format: name-1.0.2 21 | Name() string 22 | 23 | // Method returns the preferred method of the call 24 | Method() muxrpc.Method 25 | 26 | // Handler returns the muxrpc handler for the plugin 27 | Handler() muxrpc.Handler 28 | 29 | Authorizer 30 | } 31 | 32 | type PluginManager interface { 33 | Register(Plugin) 34 | MakeHandler(conn net.Conn) (muxrpc.Handler, error) 35 | } 36 | 37 | type pluginManager struct { 38 | regLock sync.Mutex // protects the map 39 | plugins map[string]Plugin 40 | } 41 | 42 | func NewPluginManager() PluginManager { 43 | return &pluginManager{ 44 | plugins: make(map[string]Plugin), 45 | } 46 | } 47 | 48 | func (pmgr *pluginManager) Register(p Plugin) { 49 | // access race 50 | pmgr.regLock.Lock() 51 | defer pmgr.regLock.Unlock() 52 | pmgr.plugins[p.Method().String()] = p 53 | } 54 | 55 | func (pmgr *pluginManager) MakeHandler(conn net.Conn) (muxrpc.Handler, error) { 56 | 57 | pmgr.regLock.Lock() 58 | defer pmgr.regLock.Unlock() 59 | 60 | h := muxrpc.HandlerMux{} 61 | 62 | for _, p := range pmgr.plugins { 63 | if !p.Authorize(conn) { 64 | continue 65 | } 66 | 67 | h.Register(p.Method(), p.Handler()) 68 | } 69 | 70 | return &h, nil 71 | } 72 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/client.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const pull = require('pull-stream') 6 | const path = require("path") 7 | const scriptname = path.basename(__filename) 8 | 9 | module.exports = (t, sbot, rpc, exit) => { 10 | // shadow t.comment to include file making the comment 11 | function comment (msg) { 12 | t.comment(`[${scriptname}] ${msg}`) 13 | } 14 | // this waits for a new incoming connection _after_ the room server is connected already 15 | // so it will be an incomming tunnel client. 16 | // since this calls exit() - if no client connects it will not exit 17 | sbot.on("rpc:connect", (remote, isClient) => { 18 | comment("tunneled connection to simple client!") 19 | 20 | // leave after 3 seconds (give the other party time to call `testing.working()`) 21 | setTimeout(() => { 22 | rpc.tunnel.leave((err, ret) => { 23 | t.error(err, 'tunnel.leave') 24 | comment(`tunnel error: ${err}`) 25 | comment(`leave value: ${ret}`) 26 | comment('left, exiting in 3s') 27 | setTimeout(exit, 3000) 28 | }) 29 | }, 1000) 30 | }) 31 | 32 | // announce ourselves to the room/tunnel 33 | rpc.tunnel.announce((err, ret) => { 34 | t.error(err, 'tunnel.announce') 35 | comment(`announce error: ${err}`) 36 | comment(`announce value: ${ret}`) 37 | comment('announced!') 38 | }) 39 | 40 | // log all new endpoints 41 | pull( 42 | rpc.tunnel.endpoints(), 43 | pull.drain(el => { 44 | comment(`from roomsrv: ${el}`) 45 | }, (err) => { 46 | t.comment('endpoints closed', err) 47 | }) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /docs/files/debian-postinstall.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # create a user to run the server as 6 | adduser --system --home /var/lib/go-ssb-room go-ssb-room 7 | chown go-ssb-room /var/lib/go-ssb-room 8 | 9 | # welcome message 10 | cat < Welcome ! 12 | 13 | go-ssb-room has been installed as a systemd service. 14 | 15 | It will store it's files (roomdb and cookie secrets) under /var/lib/go-ssb-room. 16 | This is also where you would put custom translations. 17 | 18 | For more configuration background see /usr/share/go-ssb-room/README.md 19 | or visit the code repo at https://github.com/ssbc/go-ssb-room/tree/master/docs 20 | 21 | Like outlined in that document, we highly encourage using nginx with certbot for TLS termination. 22 | We also supply an example config for this. You can find it under /usr/share/go-ssb-room/nginx-example.conf 23 | 24 | > Important 25 | 26 | Before you start using room server via the systemd service, you need to at least change the https domain in the systemd service. 27 | 28 | Edit /etc/systemd/system/go-ssb-room.service and then run this command to reflect the changes: 29 | 30 | sudo systemctl daemon-reload 31 | 32 | > Running the room server: 33 | 34 | To start/stop go-ssb-room: 35 | 36 | sudo systemctl start go-ssb-room 37 | sudo systemctl stop go-ssb-room 38 | 39 | To enable/disable go-ssb-room starting automatically on boot: 40 | 41 | sudo systemctl enable go-ssb-room 42 | sudo systemctl disable go-ssb-room 43 | 44 | To reload go-ssb-room: 45 | 46 | sudo systemctl restart go-ssb-room 47 | 48 | To view go-ssb-room logs: 49 | 50 | sudo journalctl -f -u go-ssb-room 51 | 52 | EOF 53 | -------------------------------------------------------------------------------- /internal/maybemod/testutils/logging.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package testutils 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os" 11 | "sync" 12 | "time" 13 | 14 | "go.mindeco.de/log" 15 | "go.mindeco.de/log/level" 16 | "go.mindeco.de/log/term" 17 | ) 18 | 19 | func NewRelativeTimeLogger(w io.Writer) log.Logger { 20 | if w == nil { 21 | w = log.NewSyncWriter(os.Stderr) 22 | } 23 | 24 | var rtl relTimeLogger 25 | rtl.start = time.Now() 26 | 27 | // mainLog := log.NewLogfmtLogger(w) 28 | mainLog := term.NewColorLogger(w, log.NewLogfmtLogger, colorFn) 29 | return log.With(mainLog, "t", log.Valuer(rtl.diffTime)) 30 | } 31 | 32 | func colorFn(keyvals ...interface{}) term.FgBgColor { 33 | for i := 0; i < len(keyvals); i += 2 { 34 | if key, ok := keyvals[i].(string); ok && key == "level" { 35 | lvl, ok := keyvals[i+1].(level.Value) 36 | if !ok { 37 | fmt.Printf("%d: %v %T\n", i+1, lvl, keyvals[i+1]) 38 | continue 39 | } 40 | 41 | var c term.FgBgColor 42 | level := lvl.String() 43 | switch level { 44 | case "error": 45 | c.Fg = term.Red 46 | case "warn": 47 | c.Fg = term.Brown 48 | case "debug": 49 | c.Fg = term.Gray 50 | case "info": 51 | c.Fg = term.Green 52 | default: 53 | panic("unhandled level:" + level) 54 | } 55 | return c 56 | } 57 | } 58 | return term.FgBgColor{} 59 | } 60 | 61 | type relTimeLogger struct { 62 | sync.Mutex 63 | 64 | start time.Time 65 | } 66 | 67 | func (rtl *relTimeLogger) diffTime() interface{} { 68 | rtl.Lock() 69 | defer rtl.Unlock() 70 | newStart := time.Now() 71 | since := newStart.Sub(rtl.start) 72 | return since 73 | } 74 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations/02-notices.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | -- 3 | -- SPDX-License-Identifier: CC0-1.0 4 | 5 | -- +migrate Up 6 | CREATE TABLE pins ( 7 | id integer NOT NULL PRIMARY KEY, 8 | name text NOT NULL UNIQUE 9 | ); 10 | 11 | 12 | CREATE TABLE notices ( 13 | id integer PRIMARY KEY AUTOINCREMENT NOT NULL, 14 | title text NOT NULL, 15 | content text NOT NULL, 16 | language text NOT NULL 17 | ); 18 | 19 | -- n:m relation table 20 | CREATE TABLE pin_notices ( 21 | notice_id integer NOT NULL, 22 | pin_id integer NOT NULL, 23 | 24 | PRIMARY KEY (notice_id, pin_id), 25 | 26 | FOREIGN KEY ( notice_id ) REFERENCES notices( "id" ), 27 | FOREIGN KEY ( pin_id ) REFERENCES pins( "id" ) 28 | ); 29 | 30 | -- TODO: find a better way to insert the defaults 31 | INSERT INTO pins (name) VALUES 32 | ('NoticeDescription'), 33 | ('NoticeNews'), 34 | ('NoticeCodeOfConduct'), 35 | ('NoticePrivacyPolicy'); 36 | 37 | INSERT INTO notices (title, content, language) VALUES 38 | ('Description', 'Basic description of this Room.', 'en-GB'), 39 | ('News', 'Some recent updates...', 'en-GB'), 40 | ('Code of conduct', 'We expect each other to ... 41 | * be considerate 42 | * be respectful 43 | * be responsible 44 | * be dedicated 45 | * be empathetic 46 | ', 'en-GB'), 47 | ('Privacy Policy', 'To be updated', 'en-GB'), 48 | ('Datenschutzrichtlinien', 'Bitte aktualisieren', 'de-DE'), 49 | ('Beschreibung', 'Allgemeine beschreibung des Raumes.', 'de-DE'); 50 | 51 | INSERT INTO pin_notices (notice_id, pin_id) VALUES 52 | (1, 1), 53 | (2, 2), 54 | (3, 3), 55 | (4, 4), 56 | (5, 4), 57 | (6, 1); 58 | 59 | -- +migrate Down 60 | DROP TABLE notices; 61 | DROP TABLE pins; 62 | DROP TABLE pin_notices; -------------------------------------------------------------------------------- /web/templates/auth/withssb_server_start.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | {{i18n "AuthWithSSBWelcome"}} 11 | 12 | {{i18n "AuthWithSSBTitle"}} 17 | 18 | 19 | 20 | 21 | 22 | {{if not .IsSolvingRemotely}} 23 |
24 | 25 | {{i18n "AuthWithSSBInstructQR"}} 26 | 27 | QR-Code to pass the challenge to an App 35 | {{else}} 36 |
37 | {{end}} 38 |
39 | 40 | {{if not .IsSolvingRemotely}} 41 | 42 | 43 | {{end}} 44 | {{end}} -------------------------------------------------------------------------------- /web/templates/admin/invite-revoke-confirm.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminInviteRevokeConfirmTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | {{i18n "AdminInviteRevokeConfirmWelcome"}} 15 | 16 | {{$creator := .Invite.CreatedBy.PubKey.String}} 17 | {{range $index, $alias := .Invite.CreatedBy.Aliases}} 18 | {{if eq $index 0}} 19 | {{$creator = $alias.Name}} 20 | {{end}} 21 | {{end}} 22 |
{{.Invite.CreatedAt.Format "2006-01-02T15:04:05.00"}} ({{$creator}})
25 | 26 |
27 | {{.csrfField}} 28 | 29 |
30 | {{i18n "GenericGoBack"}} 34 | 35 | 39 |
40 |
41 |
42 | {{end}} 43 | -------------------------------------------------------------------------------- /web/handlers/admin/set_language_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package admin 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/ssbc/go-ssb-room/v2/roomdb" 13 | "github.com/ssbc/go-ssb-room/v2/web/router" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | /* can't test English atm due to web/i18n/i18ntesting/testing.go:justTheKeys, which generates translations that are just 18 | * translationLabel = "translationLabel" 19 | */ 20 | // func TestLanguageSetDefaultLanguageEnglish(t *testing.T) { 21 | // ts := newSession(t) 22 | // a := assert.New(t) 23 | // 24 | // ts.ConfigDB.GetDefaultLanguageReturns("en", nil) 25 | // 26 | // u := ts.URLTo(router.AdminSettings) 27 | // html, resp := ts.Client.GetHTML(u) 28 | // a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code") 29 | // 30 | // fmt.Println(html.Html()) 31 | // summaryElement := html.Find("#language-summary") 32 | // summaryText := strings.TrimSpace(summaryElement.Text()) 33 | // a.Equal("English", summaryText, "summary language should display english translation of language name") 34 | // } 35 | 36 | func TestLanguageSetDefaultLanguage(t *testing.T) { 37 | ts := newSession(t) 38 | a := assert.New(t) 39 | 40 | ts.ConfigDB.GetDefaultLanguageReturns("de", nil) 41 | ts.User = roomdb.Member{ 42 | ID: 1234, 43 | Role: roomdb.RoleAdmin, 44 | } 45 | 46 | u := ts.URLTo(router.AdminSettings) 47 | html, resp := ts.Client.GetHTML(u) 48 | a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code") 49 | 50 | summaryElement := html.Find("#language-summary") 51 | summaryText := strings.TrimSpace(summaryElement.Text()) 52 | a.Equal("Deutsch", summaryText, "summary language should display german translation of language name") 53 | } 54 | -------------------------------------------------------------------------------- /web/styles/input.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 3 | * 4 | * SPDX-License-Identifier: CC-BY-4.0 5 | */ 6 | 7 | @tailwind base; 8 | @tailwind components; 9 | @tailwind utilities; 10 | 11 | /* Fix Flash-of-unstyled-content */ 12 | html { 13 | visibility: visible; 14 | opacity: 1; 15 | } 16 | 17 | /* custom tooltips from https://github.com/Cosbgn/tailwindcss-tooltips */ 18 | .tooltip { 19 | @apply invisible absolute; 20 | } 21 | 22 | .has-tooltip:hover .tooltip { 23 | @apply visible z-50 p-1 rounded border border-gray-200 bg-gray-100 text-gray-600 shadow-lg ml-4 text-sm; 24 | } 25 | 26 | /* custom markdown styling */ 27 | .markdown h1 { 28 | @apply text-3xl tracking-tight font-black text-black mt-2 mb-4; 29 | } 30 | 31 | .markdown h2 { 32 | @apply text-2xl tracking-tight font-black text-black mt-2 mb-3; 33 | } 34 | 35 | .markdown h3 { 36 | @apply text-xl tracking-tight font-black text-black mt-2 mb-3; 37 | } 38 | 39 | .markdown h4 { 40 | @apply text-lg tracking-tight font-black text-black mt-2 mb-3; 41 | } 42 | 43 | .markdown h5 { 44 | @apply text-base tracking-tight font-black text-black mt-2 mb-3; 45 | } 46 | 47 | .markdown h6 { 48 | @apply text-sm tracking-tight font-black text-black mt-2 mb-3; 49 | } 50 | 51 | .markdown p { 52 | @apply my-3; 53 | } 54 | 55 | .markdown a { 56 | @apply text-pink-600 underline; 57 | } 58 | 59 | .markdown ul { 60 | @apply my-3 list-disc ml-6; 61 | } 62 | 63 | .markdown ul ul { 64 | @apply my-0; 65 | } 66 | 67 | .markdown ol { 68 | @apply my-3 list-decimal ml-6; 69 | } 70 | 71 | .markdown ol ol { 72 | @apply my-0; 73 | } 74 | 75 | .markdown blockquote { 76 | @apply my-3 rounded-r-3xl px-3 py-1 bg-gray-100 text-gray-600; 77 | } 78 | 79 | .markdown hr { 80 | @apply my-5; 81 | } 82 | 83 | .markdown code { 84 | @apply break-all; 85 | } 86 | -------------------------------------------------------------------------------- /web/assets/auth-withssb-uri.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const ssbUriLink = document.querySelector('#start-auth-uri'); 6 | const waitingElem = document.querySelector('#waiting'); 7 | const errorElem = document.querySelector('#failed'); 8 | const challengeElem = document.querySelector('#challenge'); 9 | 10 | const sc = challengeElem.dataset.sc; 11 | const evtSource = new EventSource(`/withssb/events?sc=${sc}`); 12 | let otherTab; 13 | 14 | ssbUriLink.onclick = function handleURI(ev) { 15 | ev.preventDefault(); 16 | const ssbUri = ssbUriLink.href; 17 | waitingElem.classList.remove('hidden'); 18 | otherTab = window.open(ssbUri, '_blank'); 19 | }; 20 | 21 | evtSource.onerror = (e) => { 22 | waitingElem.classList.add('hidden'); 23 | errorElem.classList.remove('hidden'); 24 | console.error(e.data); 25 | if (otherTab) otherTab.close(); 26 | }; 27 | 28 | evtSource.addEventListener('failed', (e) => { 29 | waitingElem.classList.add('hidden'); 30 | errorElem.classList.remove('hidden'); 31 | console.error(e.data); 32 | if (otherTab) otherTab.close(); 33 | }); 34 | 35 | // prepare for the case that the success event happens while the browser is not on screen. 36 | let hasFocus = true; 37 | window.addEventListener('blur', () => { 38 | hasFocus = false; 39 | }); 40 | 41 | evtSource.addEventListener('success', (e) => { 42 | waitingElem.classList.add('hidden'); 43 | evtSource.close(); 44 | if (otherTab) otherTab.close(); 45 | const redirectTo = `/withssb/finalize?token=${e.data}`; 46 | if (hasFocus) { 47 | window.location.replace(redirectTo); 48 | } else { 49 | // wait for the browser to be back in focus and redirect then 50 | window.addEventListener('focus', () => { 51 | window.location.replace(redirectTo); 52 | }); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /roomdb/sqlite/models/sqlite_upsert.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.14.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/volatiletech/sqlboiler/v4/drivers" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // buildUpsertQuerySQLite builds a SQL statement string using the upsertData provided. 15 | func buildUpsertQuerySQLite(dia drivers.Dialect, tableName string, updateOnConflict bool, ret, update, conflict, whitelist []string) string { 16 | conflict = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, conflict) 17 | whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) 18 | ret = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, ret) 19 | 20 | buf := strmangle.GetBuffer() 21 | defer strmangle.PutBuffer(buf) 22 | 23 | columns := "DEFAULT VALUES" 24 | if len(whitelist) != 0 { 25 | columns = fmt.Sprintf("(%s) VALUES (%s)", 26 | strings.Join(whitelist, ", "), 27 | strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1)) 28 | } 29 | 30 | fmt.Fprintf( 31 | buf, 32 | "INSERT INTO %s %s ON CONFLICT ", 33 | tableName, 34 | columns, 35 | ) 36 | 37 | if !updateOnConflict || len(update) == 0 { 38 | buf.WriteString("DO NOTHING") 39 | } else { 40 | buf.WriteByte('(') 41 | buf.WriteString(strings.Join(conflict, ", ")) 42 | buf.WriteString(") DO UPDATE SET ") 43 | 44 | for i, v := range update { 45 | if i != 0 { 46 | buf.WriteByte(',') 47 | } 48 | quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) 49 | buf.WriteString(quoted) 50 | buf.WriteString(" = EXCLUDED.") 51 | buf.WriteString(quoted) 52 | } 53 | } 54 | 55 | if len(ret) != 0 { 56 | buf.WriteString(" RETURNING ") 57 | buf.WriteString(strings.Join(ret, ", ")) 58 | } 59 | 60 | return buf.String() 61 | } 62 | -------------------------------------------------------------------------------- /internal/aliases/confirm.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package aliases implements the validation and signing features of https://ssbc.github.io/rooms2/#alias 6 | package aliases 7 | 8 | import ( 9 | "bytes" 10 | 11 | "golang.org/x/crypto/ed25519" 12 | 13 | refs "github.com/ssbc/go-ssb-refs" 14 | ) 15 | 16 | // Registration ties an alias to the ID of the user and the RoomID it should be registered on 17 | type Registration struct { 18 | Alias string 19 | UserID refs.FeedRef 20 | RoomID refs.FeedRef 21 | } 22 | 23 | // Sign takes the public key (belonging to UserID) and returns the signed confirmation 24 | func (r Registration) Sign(privKey ed25519.PrivateKey) Confirmation { 25 | var conf Confirmation 26 | conf.Registration = r 27 | msg := r.createRegistrationMessage() 28 | conf.Signature = ed25519.Sign(privKey, msg) 29 | return conf 30 | } 31 | 32 | // createRegistrationMessage returns the string of bytes that should be signed 33 | func (r Registration) createRegistrationMessage() []byte { 34 | var message bytes.Buffer 35 | message.WriteString("=room-alias-registration:") 36 | message.WriteString(r.RoomID.String()) 37 | message.WriteString(":") 38 | message.WriteString(r.UserID.String()) 39 | message.WriteString(":") 40 | message.WriteString(r.Alias) 41 | return message.Bytes() 42 | } 43 | 44 | // Confirmation combines a registration with the corresponding signature 45 | type Confirmation struct { 46 | Registration 47 | 48 | Signature []byte 49 | } 50 | 51 | // Verify checks that the confirmation is for the expected room and from the expected feed 52 | func (c Confirmation) Verify() bool { 53 | // re-construct the registration 54 | message := c.createRegistrationMessage() 55 | 56 | // check the signature matches 57 | return ed25519.Verify(c.UserID.PubKey(), message, c.Signature) 58 | } 59 | -------------------------------------------------------------------------------- /web/templates/admin/denied-keys-remove-confirm.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AdminDeniedKeysRemoveConfirmTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | 11 | {{i18n "AdminDeniedKeysRemoveConfirmWelcome"}} 15 | 16 |
{{.Entry.PubKey.String}}
20 | 21 |
22 | {{human_time .Entry.CreatedAt}} 23 | {{.Entry.CreatedAt.Format "2006-01-02T15:04:05.00"}} 24 |
25 | 26 | {{i18n "AdminDeniedKeysCommentDescription"}} 30 |

{{.Entry.Comment}}

31 | 32 |
33 | {{ .csrfField }} 34 | 35 |
36 | {{i18n "GenericGoBack"}} 40 | 41 | 45 |
46 |
47 |
48 | {{end}} 49 | -------------------------------------------------------------------------------- /internal/network/conntracker_acceptAll.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package network 6 | 7 | import ( 8 | "context" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // This just keeps a count and doesn't actually track anything 15 | func NewAcceptAllTracker() ConnTracker { 16 | return &acceptAllTracker{} 17 | } 18 | 19 | type acceptAllTracker struct { 20 | countLock sync.Mutex 21 | conns []net.Conn 22 | } 23 | 24 | func (ct *acceptAllTracker) CloseAll() { 25 | ct.countLock.Lock() 26 | defer ct.countLock.Unlock() 27 | for _, c := range ct.conns { 28 | c.Close() 29 | } 30 | ct.conns = []net.Conn{} 31 | } 32 | 33 | func (ct *acceptAllTracker) Count() uint { 34 | ct.countLock.Lock() 35 | defer ct.countLock.Unlock() 36 | return uint(len(ct.conns)) 37 | } 38 | 39 | func (ct *acceptAllTracker) Active(a net.Addr) (bool, time.Duration) { 40 | ct.countLock.Lock() 41 | defer ct.countLock.Unlock() 42 | for _, c := range ct.conns { 43 | if sameByRemote(c, a) { 44 | return true, 0 45 | } 46 | } 47 | return false, 0 48 | } 49 | 50 | func (ct *acceptAllTracker) OnAccept(ctx context.Context, conn net.Conn) (bool, context.Context) { 51 | ct.countLock.Lock() 52 | defer ct.countLock.Unlock() 53 | ct.conns = append(ct.conns, conn) 54 | return true, ctx 55 | } 56 | 57 | func (ct *acceptAllTracker) OnClose(conn net.Conn) time.Duration { 58 | ct.countLock.Lock() 59 | defer ct.countLock.Unlock() 60 | for i, c := range ct.conns { 61 | 62 | if sameByRemote(c, conn.RemoteAddr()) { 63 | // remove from array, replace style 64 | ct.conns[i] = ct.conns[len(ct.conns)-1] 65 | ct.conns[len(ct.conns)-1] = nil 66 | ct.conns = ct.conns[:len(ct.conns)-1] 67 | return 1 68 | } 69 | 70 | } 71 | 72 | return 0 73 | } 74 | 75 | func sameByRemote(a net.Conn, b net.Addr) bool { 76 | return a.RemoteAddr().String() == b.String() 77 | } 78 | -------------------------------------------------------------------------------- /web/templates/invite/facade-fallback.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }} 8 | {{ define "content" }} 9 |
10 |

{{i18n "SSBURIFailureWelcome"}}

11 | 12 | 18 | 19 |
20 | 21 |

Or if you already have an SSB app (such as Patchwork, Patchbay, Patchfoo, Oasis, Planetary) and it couldn't process the link for whatever reason, you can manually input your identifier here:

24 | 25 | {{i18n "InviteFacadeFallbackInsertID"}} 29 |
30 | {{ end }} -------------------------------------------------------------------------------- /internal/signinwithssb/bridge_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package signinwithssb 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestBridgeWorked(t *testing.T) { 16 | t.Parallel() 17 | 18 | a := assert.New(t) 19 | 20 | sb := NewSignalBridge() 21 | 22 | // try to use a non-existant session 23 | err := sb.SessionWorked("nope", "just a test") 24 | a.Error(err) 25 | 26 | // make a new session 27 | sc := sb.RegisterSession() 28 | 29 | b, err := DecodeChallengeString(sc) 30 | a.NoError(err) 31 | a.Len(b, challengeLength) 32 | 33 | updates, has := sb.GetEventChannel(sc) 34 | a.True(has) 35 | 36 | go func() { 37 | err := sb.SessionWorked(sc, "a token") 38 | a.NoError(err) 39 | }() 40 | 41 | time.Sleep(time.Second / 4) 42 | 43 | select { 44 | case evt := <-updates: 45 | a.True(evt.Worked) 46 | a.Equal("a token", evt.Token) 47 | a.Nil(evt.Reason) 48 | default: 49 | t.Error("no updates") 50 | } 51 | } 52 | 53 | func TestBridgeFailed(t *testing.T) { 54 | t.Parallel() 55 | 56 | a := assert.New(t) 57 | 58 | sb := NewSignalBridge() 59 | 60 | // try to use a non-existant session 61 | testReason := fmt.Errorf("just an error") 62 | err := sb.SessionFailed("nope", testReason) 63 | a.Error(err) 64 | 65 | // make a new session 66 | sc := sb.RegisterSession() 67 | 68 | b, err := DecodeChallengeString(sc) 69 | a.NoError(err) 70 | a.Len(b, challengeLength) 71 | 72 | updates, has := sb.GetEventChannel(sc) 73 | a.True(has) 74 | 75 | go func() { 76 | err := sb.SessionFailed(sc, testReason) 77 | a.NoError(err) 78 | }() 79 | 80 | time.Sleep(time.Second / 4) 81 | 82 | select { 83 | case evt := <-updates: 84 | a.False(evt.Worked) 85 | a.Equal("", evt.Token) 86 | a.EqualError(testReason, evt.Reason.Error()) 87 | default: 88 | t.Error("no updates") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Go-SSB Room 8 | [![REUSE status](https://api.reuse.software/badge/github.com/ssbc/go-ssb-room)](https://api.reuse.software/info/github.com/ssbc/go-ssb-room) 9 | 10 | This repository implements the [Room (v1+v2) server spec](https://github.com/ssbc/rooms2), in Go. 11 | 12 | It includes: 13 | * secret-handshake+boxstream network transport, sometimes referred to as SHS, using [secretstream](https://github.com/ssbc/go-secretstream) 14 | * muxrpc handlers for tunneling connections 15 | * a fully embedded HTTP server & HTML frontend, for administering the room 16 | 17 | ![](./docs/images/screenshot.png) 18 | 19 | See [this project](https://github.com/orgs/ssbc/projects/2) for current focus. 20 | 21 | ## :star: Features 22 | 23 | * Rooms v1 (`tunnel.connect`, `tunnel.endpoints`, etc.) 24 | * User management (allow- & denylisting + moderator & administrator roles), all administered via the web dashboard 25 | * Multiple [privacy modes](https://ssbc.github.io/rooms2/#privacy-modes) 26 | * [Sign-in with SSB](https://ssbc.github.io/ssb-http-auth-spec/) 27 | * [HTTP Invites](https://github.com/ssbc/ssb-http-invite-spec) 28 | * Alias management 29 | 30 | For a comprehensive introduction to rooms 2.0, 🎥 [watch this video](https://www.youtube.com/watch?v=W5p0y_MWwDE). 31 | For a description of MuxRPC APIs see https://github.com/ssbc/rooms2 32 | 33 | ## :rocket: Deployment 34 | 35 | If you want to deploy a room server yourself, follow our [deployment.md](./docs/deployment.md) docs. 36 | 37 | ## :wrench: Development 38 | 39 | For an in-depth codebase walkthrough, see the [development.md](./docs/development.md) file in the `docs` folder of this repository. 40 | 41 | ## :people_holding_hands: Authors 42 | 43 | * [cryptix](https://github.com/cryptix) (`@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519`) 44 | * [staltz](https://github.com/staltz) 45 | * [cblgh](https://github.com/cblgh) 46 | 47 | ## License 48 | 49 | MIT 50 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/aliases_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package nodejs_test 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/ssbc/go-ssb-room/v2/internal/aliases" 15 | "github.com/ssbc/go-ssb-room/v2/roomdb" 16 | "github.com/ssbc/go-ssb-room/v2/roomdb/mockdb" 17 | ) 18 | 19 | func TestGoServerJSClientAliases(t *testing.T) { 20 | a := assert.New(t) 21 | r := require.New(t) 22 | 23 | ts := newRandomSession(t) 24 | // ts := newSession(t, nil) 25 | 26 | var membersDB = &mockdb.FakeMembersService{} 27 | var aliasesDB = &mockdb.FakeAliasesService{} 28 | srv := ts.startGoServer(membersDB, aliasesDB) 29 | // allow all peers (there arent any we dont want to allow) 30 | membersDB.GetByFeedReturns(roomdb.Member{ID: 1234}, nil) 31 | 32 | // setup mocks for this test 33 | aliasesDB.RegisterReturns(nil) 34 | 35 | alice := ts.startJSClient("alice", "./testscripts/modern_aliases.js", 36 | srv.Network.GetListenAddr(), 37 | srv.Whoami(), 38 | ) 39 | 40 | // the revoke call checks who the alias belongs to 41 | aliasesDB.ResolveReturns(roomdb.Alias{ 42 | Name: "alice", 43 | Feed: alice, 44 | }, nil) 45 | 46 | time.Sleep(5 * time.Second) 47 | 48 | // wait for both to exit 49 | ts.wait() 50 | 51 | r.Equal(1, aliasesDB.RegisterCallCount(), "register call count") 52 | _, name, ref, signature := aliasesDB.RegisterArgsForCall(0) 53 | a.Equal("alice", name, "wrong alias registered") 54 | a.Equal(alice.String(), ref.String()) 55 | 56 | var aliasReq aliases.Confirmation 57 | aliasReq.Alias = name 58 | aliasReq.Signature = signature 59 | aliasReq.UserID = alice 60 | aliasReq.RoomID = srv.Whoami() 61 | 62 | a.True(aliasReq.Verify(), "signature validation") 63 | 64 | r.Equal(1, aliasesDB.RevokeCallCount(), "revoke call count") 65 | _, name = aliasesDB.RevokeArgsForCall(0) 66 | a.Equal("alice", name, "wrong alias revoked") 67 | 68 | } 69 | -------------------------------------------------------------------------------- /web/templates/change-member-password.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{ i18n "AuthFallbackPasswordChangeFormTitle" }}{{ end }} 8 | {{ define "content" }} 9 |
10 | {{i18n "AuthFallbackPasswordChangeWelcome"}} 11 | 12 | {{ template "flashes" . }} 13 | 14 |
20 | {{.csrfField}} 21 | 22 | {{if ne .ResetToken ""}} 23 | 24 | {{end}} 25 | 26 | 30 | 35 | 36 | 40 | 45 | 46 | 47 | 51 |
52 |
53 | {{ end }} 54 | -------------------------------------------------------------------------------- /web/router/complete.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package router 6 | 7 | import ( 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // constant names for the named routes 12 | const ( 13 | CompleteIndex = "complete:index" 14 | 15 | CompleteNoticeShow = "complete:notice:show" 16 | CompleteNoticeList = "complete:notice:list" 17 | 18 | CompleteSetLanguage = "complete:set-language" 19 | 20 | CompleteAliasResolve = "complete:alias:resolve" 21 | 22 | CompleteInviteFacade = "complete:invite:accept" 23 | CompleteInviteFacadeFallback = "complete:invite:accept:fallback" 24 | CompleteInviteInsertID = "complete:invite:insert-id" 25 | CompleteInviteConsume = "complete:invite:consume" 26 | 27 | MembersChangePasswordForm = "members:change-password:form" 28 | MembersChangePassword = "members:change-password" 29 | 30 | OpenModeCreateInvite = "open:invites:create" 31 | ) 32 | 33 | // CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend 34 | func CompleteApp() *mux.Router { 35 | m := mux.NewRouter() 36 | 37 | Auth(m) 38 | Admin(m.PathPrefix("/admin").Subrouter()) 39 | 40 | m.Path("/").Methods("GET").Name(CompleteIndex) 41 | 42 | m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve) 43 | 44 | m.Path("/members/change-password").Methods("GET").Name(MembersChangePasswordForm) 45 | m.Path("/members/change-password").Methods("POST").Name(MembersChangePassword) 46 | 47 | m.Path("/create-invite").Methods("GET", "POST").Name(OpenModeCreateInvite) 48 | m.Path("/join").Methods("GET").Name(CompleteInviteFacade) 49 | m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback) 50 | m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID) 51 | m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume) 52 | 53 | m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow) 54 | m.Path("/notice/list").Methods("GET").Name(CompleteNoticeList) 55 | 56 | m.Path("/set-language").Methods("POST").Name(CompleteSetLanguage) 57 | 58 | return m 59 | } 60 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations/04-overhaul-fallback-auth.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | -- 3 | -- SPDX-License-Identifier: CC0-1.0 4 | 5 | -- +migrate Up 6 | 7 | -- drop login column from fallback pw 8 | -- ================================== 9 | 10 | -- this is sqlite style ALTER TABLE abc DROP COLUMN 11 | -- See 5) in https://www.sqlite.org/lang_altertable.html 12 | -- and https://www.sqlitetutorial.net/sqlite-alter-table/ 13 | 14 | -- drop obsolete index 15 | DROP INDEX fallback_passwords_by_login; 16 | 17 | -- create new schema table (without 'login' column) 18 | CREATE TABLE updated_passwords_table ( 19 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 20 | password_hash BLOB NOT NULL, 21 | 22 | member_id INTEGER UNIQUE NOT NULL, 23 | 24 | FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE 25 | ); 26 | 27 | -- copy existing values from original table into new 28 | INSERT INTO updated_passwords_table(id, password_hash, member_id) 29 | SELECT id, password_hash, member_id 30 | FROM fallback_passwords; 31 | 32 | -- rename the new to the original table name 33 | DROP TABLE fallback_passwords; 34 | ALTER TABLE updated_passwords_table RENAME TO fallback_passwords; 35 | 36 | -- create new lookup index by member id 37 | CREATE INDEX fallback_passwords_by_member ON fallback_passwords(member_id); 38 | 39 | -- add new table for password reset tokens 40 | --======================================== 41 | CREATE TABLE fallback_reset_tokens ( 42 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 43 | hashed_token TEXT UNIQUE NOT NULL, 44 | created_by INTEGER NOT NULL, 45 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 46 | 47 | for_member INTEGER NOT NULL, 48 | 49 | active boolean NOT NULL DEFAULT TRUE, 50 | 51 | FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE 52 | FOREIGN KEY ( for_member ) REFERENCES members( "id" ) ON DELETE CASCADE 53 | ); 54 | CREATE INDEX fallback_reset_tokens_by_token ON fallback_reset_tokens(hashed_token); 55 | 56 | -- +migrate Down 57 | DROP INDEX fallback_passwords_by_member; 58 | DROP TABLE fallback_passwords; 59 | 60 | DROP TABLE fallback_reset_tokens; -------------------------------------------------------------------------------- /internal/network/isserver_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package network_test 6 | 7 | import ( 8 | "context" 9 | "crypto/rand" 10 | "net" 11 | "os" 12 | "testing" 13 | 14 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/keys" 15 | "github.com/ssbc/go-ssb-room/v2/internal/network" 16 | "go.mindeco.de/log" 17 | 18 | "github.com/ssbc/go-muxrpc/v2" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestIsServer(t *testing.T) { 24 | r := require.New(t) 25 | 26 | ctx := context.Background() 27 | 28 | var appkey = make([]byte, 32) 29 | rand.Read(appkey) 30 | 31 | logger := log.NewLogfmtLogger(os.Stderr) 32 | 33 | kpClient, err := keys.NewKeyPair(nil) 34 | r.NoError(err) 35 | 36 | kpServ, err := keys.NewKeyPair(nil) 37 | r.NoError(err) 38 | 39 | client, err := network.New(network.Options{ 40 | Logger: logger, 41 | AppKey: appkey, 42 | KeyPair: kpClient, 43 | 44 | MakeHandler: makeServerHandler(t, true), 45 | }) 46 | r.NoError(err) 47 | 48 | server, err := network.New(network.Options{ 49 | Logger: logger, 50 | AppKey: appkey, 51 | KeyPair: kpServ, 52 | 53 | ListenAddr: &net.TCPAddr{Port: 0}, // any random port 54 | 55 | MakeHandler: makeServerHandler(t, false), 56 | }) 57 | r.NoError(err) 58 | 59 | go func() { 60 | err = server.Serve(ctx) 61 | if err != nil { 62 | panic(err) 63 | } 64 | }() 65 | 66 | err = client.Connect(ctx, server.GetListenAddr()) 67 | r.NoError(err) 68 | 69 | client.Close() 70 | server.Close() 71 | } 72 | 73 | type testHandler struct { 74 | t *testing.T 75 | wantServer bool 76 | } 77 | 78 | func (testHandler) Handled(muxrpc.Method) bool { return true } 79 | 80 | func (th testHandler) HandleConnect(ctx context.Context, e muxrpc.Endpoint) { 81 | require.Equal(th.t, th.wantServer, muxrpc.IsServer(e), "server assertion failed") 82 | } 83 | 84 | func (th testHandler) HandleCall(ctx context.Context, req *muxrpc.Request) {} 85 | 86 | func makeServerHandler(t *testing.T, wantServer bool) func(net.Conn) (muxrpc.Handler, error) { 87 | return func(_ net.Conn) (muxrpc.Handler, error) { 88 | return testHandler{t, wantServer}, nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /roomsrv/init_network.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package roomsrv 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | 11 | "github.com/ssbc/go-muxrpc/v2" 12 | 13 | "github.com/ssbc/go-ssb-room/v2/internal/network" 14 | "github.com/ssbc/go-ssb-room/v2/roomdb" 15 | ) 16 | 17 | // opens the shs listener for TCP connections 18 | func (s *Server) initNetwork() error { 19 | // muxrpc handler creation and authoratization decider 20 | mkHandler := func(conn net.Conn) (muxrpc.Handler, error) { 21 | s.closedMu.Lock() 22 | defer s.closedMu.Unlock() 23 | 24 | remote, err := network.GetFeedRefFromAddr(conn.RemoteAddr()) 25 | if err != nil { 26 | return nil, fmt.Errorf("sbot: expected an address containing an shs-bs addr: %w", err) 27 | } 28 | 29 | if s.keyPair.Feed.Equal(remote) { 30 | return &s.master, nil 31 | } 32 | 33 | pm, err := s.Config.GetPrivacyMode(s.rootCtx) 34 | if err != nil { 35 | return nil, fmt.Errorf("running with unknown privacy mode") 36 | } 37 | 38 | // if privacy mode is restricted, deny connections from non-members 39 | if pm == roomdb.ModeRestricted { 40 | if _, err := s.Members.GetByFeed(s.rootCtx, remote); err != nil { 41 | return nil, fmt.Errorf("access restricted to members") 42 | } 43 | } 44 | 45 | // if feed is in the deny list, deny their connection 46 | if s.DeniedKeys.HasFeed(s.rootCtx, remote) { 47 | return nil, fmt.Errorf("this key has been banned") 48 | } 49 | 50 | // for community + open modes, allow all connections 51 | return &s.public, nil 52 | } 53 | 54 | // tcp+shs 55 | opts := network.Options{ 56 | Logger: s.logger, 57 | Dialer: s.dialer, 58 | ListenAddr: s.listenAddr, 59 | KeyPair: s.keyPair, 60 | AppKey: s.appKey[:], 61 | MakeHandler: mkHandler, 62 | ConnTracker: s.networkConnTracker, 63 | BefreCryptoWrappers: s.preSecureWrappers, 64 | AfterSecureWrappers: s.postSecureWrappers, 65 | } 66 | 67 | var err error 68 | s.Network, err = network.New(opts) 69 | if err != nil { 70 | return fmt.Errorf("failed to create network node: %w", err) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /muxrpc/handlers/tunnel/server/plugin.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package server 6 | 7 | import ( 8 | "github.com/ssbc/go-muxrpc/v2" 9 | "github.com/ssbc/go-muxrpc/v2/typemux" 10 | kitlog "go.mindeco.de/log" 11 | 12 | "github.com/ssbc/go-ssb-room/v2/internal/network" 13 | "github.com/ssbc/go-ssb-room/v2/roomdb" 14 | "github.com/ssbc/go-ssb-room/v2/roomstate" 15 | ) 16 | 17 | /* manifest: 18 | { 19 | "announce": "sync", 20 | "leave": "sync", 21 | "connect": "duplex", 22 | "endpoints": "source", 23 | "isRoom": "async", 24 | "ping": "sync", 25 | } 26 | */ 27 | 28 | func New(log kitlog.Logger, netInfo network.ServerEndpointDetails, m *roomstate.Manager, members roomdb.MembersService, config roomdb.RoomConfig) *Handler { 29 | var h = new(Handler) 30 | h.netInfo = netInfo 31 | h.logger = log 32 | h.state = m 33 | h.membersdb = members 34 | h.config = config 35 | 36 | return h 37 | } 38 | 39 | func (h *Handler) RegisterTunnel(mux typemux.HandlerMux) { 40 | var namespace = muxrpc.Method{"tunnel"} 41 | mux.RegisterAsync(append(namespace, "isRoom"), typemux.AsyncFunc(h.metadata)) 42 | mux.RegisterAsync(append(namespace, "ping"), typemux.AsyncFunc(h.ping)) 43 | 44 | mux.RegisterAsync(append(namespace, "announce"), typemux.AsyncFunc(h.announce)) 45 | mux.RegisterAsync(append(namespace, "leave"), typemux.AsyncFunc(h.leave)) 46 | 47 | mux.RegisterSource(append(namespace, "endpoints"), typemux.SourceFunc(h.endpoints)) 48 | 49 | mux.RegisterDuplex(append(namespace, "connect"), connectHandler{ 50 | logger: h.logger, 51 | self: h.netInfo.RoomID, 52 | state: h.state, 53 | }) 54 | } 55 | 56 | func (h *Handler) RegisterRoom(mux typemux.HandlerMux) { 57 | var namespace = muxrpc.Method{"room"} 58 | mux.RegisterAsync(append(namespace, "metadata"), typemux.AsyncFunc(h.metadata)) 59 | mux.RegisterAsync(append(namespace, "ping"), typemux.AsyncFunc(h.ping)) 60 | 61 | mux.RegisterSource(append(namespace, "attendants"), typemux.SourceFunc(h.attendants)) 62 | mux.RegisterSource(append(namespace, "members"), typemux.SourceFunc(h.members)) 63 | 64 | mux.RegisterDuplex(append(namespace, "connect"), connectHandler{ 65 | logger: h.logger, 66 | self: h.netInfo.RoomID, 67 | state: h.state, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /web/templates/admin/notice-edit.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "NoticeEditTitle"}}{{ end }} 8 | {{ define "content" }} 9 | 10 | {{ template "flashes" . }} 11 | 12 |
13 | {{.csrfField}} 14 | 15 | {{if .PinnedName}} 16 | 20 | 24 | {{else}} 25 | 29 | 33 | {{end}} 34 | 35 | 41 | 48 | 49 |
50 | 51 | 56 | TODO: make this a dropdown 57 |
58 | 59 | 63 |
64 | {{end}} -------------------------------------------------------------------------------- /web/errors/badrequest.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package errors defines some well defined errors, like incomplete/wrong request data or object not found(404), for the purpose of internationalization. 6 | package errors 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | ) 12 | 13 | var ( 14 | ErrNotAuthorized = errors.New("rooms/web: not authorized") 15 | 16 | ErrDenied = errors.New("rooms: this key has been banned") 17 | ) 18 | 19 | // ErrGenericLocalized is used for one-off errors that primarily are presented for the user. 20 | // The contained label is passed to the i18n engine for translation. 21 | type ErrGenericLocalized struct{ Label string } 22 | 23 | func (err ErrGenericLocalized) Error() string { 24 | return fmt.Sprintf("rooms/web: localized error (%s)", err.Label) 25 | } 26 | 27 | type ErrNotFound struct{ What string } 28 | 29 | func (nf ErrNotFound) Error() string { 30 | return fmt.Sprintf("rooms/web: item not found: %s", nf.What) 31 | } 32 | 33 | type ErrBadRequest struct { 34 | Where string 35 | Details error 36 | } 37 | 38 | func (err ErrBadRequest) Unwrap() error { 39 | return err.Details 40 | } 41 | 42 | func (br ErrBadRequest) Error() string { 43 | return fmt.Sprintf("rooms/web: bad request error: %s", br.Details) 44 | } 45 | 46 | type ErrForbidden struct{ Details error } 47 | 48 | func (f ErrForbidden) Error() string { 49 | return fmt.Sprintf("rooms/web: access denied: %s", f.Details) 50 | } 51 | 52 | // ErrRedirect is used when the controller decides to not render a page 53 | type ErrRedirect struct { 54 | Path string 55 | 56 | // reason will be added as a flash error 57 | Reason error 58 | } 59 | 60 | func (err ErrRedirect) Unwrap() error { 61 | return err.Reason 62 | } 63 | 64 | func (err ErrRedirect) Error() string { 65 | return fmt.Sprintf("rooms/web: redirecting to: %s (reason: %s)", err.Path, err.Reason) 66 | } 67 | 68 | type PageNotFound struct{ Path string } 69 | 70 | func (e PageNotFound) Error() string { 71 | return fmt.Sprintf("rooms/web: page not found: %s", e.Path) 72 | } 73 | 74 | type DatabaseError struct{ Reason error } 75 | 76 | func (e DatabaseError) Error() string { 77 | return fmt.Sprintf("rooms/web: database failed to complete query: %s", e.Reason.Error()) 78 | } 79 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/testscripts/client-opening-tunnel.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const pull = require('pull-stream') 6 | const { readFileSync } = require('fs') 7 | const path = require("path") 8 | const scriptname = path.basename(__filename) 9 | 10 | let newConnections = 0 11 | 12 | module.exports = (t, client, roomrpc, exit) => { 13 | // shadow t.comment to include file making the comment 14 | function comment (msg) { 15 | t.comment(`[${scriptname}] ${msg}`) 16 | } 17 | newConnections++ 18 | comment(`new connection: ${roomrpc.id}`) 19 | comment(`total connections: ${newConnections}`) 20 | 21 | if (newConnections > 1) { 22 | comment('more than two connnections, not doing anything') 23 | return 24 | } 25 | 26 | // we are now connected to the room server. 27 | // log all new endpoints 28 | pull( 29 | roomrpc.tunnel.endpoints(), 30 | pull.drain(el => { 31 | comment(`from roomsrv: ${JSON.stringify(el)}`) 32 | }, (err) => { 33 | t.comment('endpoints closed', err) 34 | }) 35 | ) 36 | 37 | // give the room time to start 38 | setTimeout(() => { 39 | // announce ourselves to the room/tunnel 40 | roomrpc.tunnel.announce((err, ret) => { 41 | t.error(err, 'announce on server') 42 | comment('announced!') 43 | 44 | // put there by the go test process 45 | let roomHandle = readFileSync('endpoint_through_room.txt').toString() 46 | comment(`connecting to room handle: ${roomHandle}`) 47 | 48 | client.conn.connect(roomHandle, (err, tunneledrpc) => { 49 | t.error(err, 'connect through room') 50 | comment(`got a tunnel to: ${tunneledrpc.id}`) 51 | 52 | // check the tunnel connection works 53 | tunneledrpc.testing.working((err, ok) => { 54 | t.error(err, 'testing.working didnt error') 55 | t.true(ok, 'testing.working is true') 56 | 57 | // start leaving after 2s 58 | setTimeout(() => { 59 | roomrpc.tunnel.leave((err, ret) => { 60 | t.error(err, 'tunnel.leave') 61 | comment('left room... exiting in 1s') 62 | setTimeout(exit, 1000) 63 | }) 64 | }, 2000) 65 | }) 66 | }) 67 | }) 68 | }, 5000) 69 | } 70 | -------------------------------------------------------------------------------- /internal/aliases/confirm_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package aliases 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | 11 | refs "github.com/ssbc/go-ssb-refs" 12 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/keys" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestConfirmation(t *testing.T) { 17 | r := require.New(t) 18 | 19 | // this is our room, it's not a valid feed but thats fine for this test 20 | roomID, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte("test"), 8), refs.RefAlgoFeedSSB1) 21 | if err != nil { 22 | r.Error(err) 23 | } 24 | 25 | // to make the test deterministic, decided by fair dice roll. 26 | seed := bytes.Repeat([]byte("yeah"), 8) 27 | // our user, who will sign the registration 28 | userKeyPair, err := keys.NewKeyPair(bytes.NewReader(seed)) 29 | r.NoError(err) 30 | 31 | // create and fill out the registration for an alias (in this case the name of the test) 32 | var valid Registration 33 | valid.RoomID = roomID 34 | valid.UserID = userKeyPair.Feed 35 | valid.Alias = t.Name() 36 | 37 | // internal function to create the registration string 38 | msg := valid.createRegistrationMessage() 39 | want := "=room-alias-registration:@dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3Q=.ed25519:@Rt2aJrtOqWXhBZ5/vlfzeWQ9Bj/z6iT8CMhlr2WWlG4=.ed25519:TestConfirmation" 40 | r.Equal(want, string(msg)) 41 | 42 | // create the signed confirmation 43 | confirmation := valid.Sign(userKeyPair.Pair.Secret) 44 | 45 | yes := confirmation.Verify() 46 | r.True(yes, "should be valid for this room and feed") 47 | 48 | // make up another id for the invalid test(s) 49 | otherID, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte("nope"), 8), refs.RefAlgoFeedSSB1) 50 | if err != nil { 51 | r.Error(err) 52 | } 53 | 54 | confirmation.RoomID = otherID 55 | yes = confirmation.Verify() 56 | r.False(yes, "should not be valid for another room") 57 | 58 | confirmation.RoomID = roomID // restore 59 | confirmation.UserID = otherID 60 | yes = confirmation.Verify() 61 | r.False(yes, "should not be valid for this room but another feed") 62 | 63 | // puncture the signature to emulate an invalid one 64 | confirmation.Signature[0] = confirmation.Signature[0] ^ 1 65 | 66 | yes = confirmation.Verify() 67 | r.False(yes, "should not be valid anymore") 68 | 69 | } 70 | -------------------------------------------------------------------------------- /web/i18n/i18ntesting/testing.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package i18ntesting 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/BurntSushi/toml" 16 | 17 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 18 | "github.com/ssbc/go-ssb-room/v2/web/i18n" 19 | ) 20 | 21 | // justTheKeys auto generates from the defaults a list of Label = "Label" 22 | // must keep order of input intact 23 | // (at least all the globals before starting with nested plurals) 24 | // also replaces 'one' and 'other' in plurals 25 | func justTheKeys(t *testing.T) []byte { 26 | f, err := i18n.Defaults.Open("active.en.toml") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | justAMap := make(map[string]interface{}) 31 | md, err := toml.DecodeReader(f, &justAMap) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | var buf = &bytes.Buffer{} 37 | 38 | // if we don't produce the same order as the input 39 | // (in go maps are ALWAYS random access when ranged over) 40 | // nested keys (such as plural form) will mess up the global level... 41 | for _, k := range md.Keys() { 42 | key := k.String() 43 | val, has := justAMap[key] 44 | if !has { 45 | // fmt.Println("i18n test warning:", key, "not unmarshaled") 46 | continue 47 | } 48 | 49 | switch tv := val.(type) { 50 | 51 | case string: 52 | fmt.Fprintf(buf, "%s = \"%s\"\n", key, key) 53 | 54 | case map[string]interface{}: 55 | // fmt.Println("i18n test warning: custom map for ", key) 56 | 57 | fmt.Fprintf(buf, "\n[%s]\n", key) 58 | // replace "one" and "other" keys 59 | // with Label and LabelPlural 60 | tv["one"] = key + "Singular" 61 | tv["other"] = key + "Plural" 62 | toml.NewEncoder(buf).Encode(tv) 63 | fmt.Fprintln(buf) 64 | 65 | default: 66 | t.Fatalf("unhandled toml structure under %s: %T\n", key, val) 67 | } 68 | } 69 | 70 | return buf.Bytes() 71 | } 72 | 73 | func WriteReplacement(t *testing.T) { 74 | r := repo.New(filepath.Join("testrun", t.Name())) 75 | 76 | testOverride := filepath.Join(r.GetPath("i18n"), "active.en.toml") 77 | t.Log(testOverride) 78 | os.MkdirAll(filepath.Dir(testOverride), 0700) 79 | 80 | content := justTheKeys(t) 81 | 82 | err := ioutil.WriteFile(testOverride, content, 0700) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/signinwithssb/challenges.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package signinwithssb 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rand" 10 | "encoding/base64" 11 | "fmt" 12 | 13 | "golang.org/x/crypto/ed25519" 14 | 15 | refs "github.com/ssbc/go-ssb-refs" 16 | ) 17 | 18 | // sign-in with ssb uses 256-bit nonces 19 | const challengeLength = 32 20 | 21 | // DecodeChallengeString accepts base64 encoded strings and decodes them, 22 | // checks their length to be equal to challengeLength, 23 | // and returns the decoded bytes 24 | func DecodeChallengeString(c string) ([]byte, error) { 25 | challengeBytes, err := base64.URLEncoding.DecodeString(c) 26 | if err != nil { 27 | return nil, fmt.Errorf("invalid challenge encoding: %w", err) 28 | } 29 | 30 | if n := len(challengeBytes); n != challengeLength { 31 | return nil, fmt.Errorf("invalid challenge length: expected %d but got %d", challengeLength, n) 32 | } 33 | 34 | return challengeBytes, nil 35 | } 36 | 37 | // GenerateChallenge returs a base64 encoded string 38 | // with challangeLength bytes of random data 39 | func GenerateChallenge() string { 40 | buf := make([]byte, challengeLength) 41 | rand.Read(buf) 42 | return base64.URLEncoding.EncodeToString(buf) 43 | } 44 | 45 | // ClientPayload is used to create and verify solutions 46 | type ClientPayload struct { 47 | ClientID, ServerID refs.FeedRef 48 | 49 | ClientChallenge string 50 | ServerChallenge string 51 | } 52 | 53 | // recreate the signed message 54 | func (cr ClientPayload) createMessage() []byte { 55 | var msg bytes.Buffer 56 | msg.WriteString("=http-auth-sign-in:") 57 | msg.WriteString(cr.ServerID.String()) 58 | msg.WriteString(":") 59 | msg.WriteString(cr.ClientID.String()) 60 | msg.WriteString(":") 61 | msg.WriteString(cr.ServerChallenge) 62 | msg.WriteString(":") 63 | msg.WriteString(cr.ClientChallenge) 64 | return msg.Bytes() 65 | } 66 | 67 | // Sign returns the signature created with the passed privateKey 68 | func (cr ClientPayload) Sign(privateKey ed25519.PrivateKey) []byte { 69 | msg := cr.createMessage() 70 | return ed25519.Sign(privateKey, msg) 71 | } 72 | 73 | // Validate checks the signature by calling createMessage() and ed25519.Verify() 74 | // together with the ClientID public key. 75 | func (cr ClientPayload) Validate(signature []byte) bool { 76 | msg := cr.createMessage() 77 | return ed25519.Verify(cr.ClientID.PubKey(), msg, signature) 78 | } 79 | -------------------------------------------------------------------------------- /roomsrv/init_handlers.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package roomsrv 6 | 7 | import ( 8 | muxrpc "github.com/ssbc/go-muxrpc/v2" 9 | "github.com/ssbc/go-muxrpc/v2/typemux" 10 | kitlog "go.mindeco.de/log" 11 | 12 | "github.com/ssbc/go-ssb-room/v2/muxrpc/handlers/alias" 13 | "github.com/ssbc/go-ssb-room/v2/muxrpc/handlers/gossip" 14 | "github.com/ssbc/go-ssb-room/v2/muxrpc/handlers/signinwithssb" 15 | "github.com/ssbc/go-ssb-room/v2/muxrpc/handlers/tunnel/server" 16 | "github.com/ssbc/go-ssb-room/v2/muxrpc/handlers/whoami" 17 | ) 18 | 19 | // instantiate and register the muxrpc handlers 20 | func (s *Server) initHandlers() { 21 | // inistaniate handler packages 22 | whoami := whoami.New(s.Whoami()) 23 | 24 | tunnelHandler := server.New( 25 | kitlog.With(s.logger, "unit", "tunnel"), 26 | s.netInfo, 27 | s.StateManager, 28 | s.Members, 29 | s.Config, 30 | ) 31 | 32 | aliasHandler := alias.New( 33 | kitlog.With(s.logger, "unit", "aliases"), 34 | s.Whoami(), 35 | s.Aliases, 36 | s.netInfo, 37 | ) 38 | 39 | siwssbHandler := signinwithssb.New( 40 | kitlog.With(s.logger, "unit", "auth-with-ssb"), 41 | s.Whoami(), 42 | s.netInfo.Domain, 43 | s.Members, 44 | s.authWithSSB, 45 | s.authWithSSBBridge, 46 | ) 47 | 48 | // register muxrpc commands 49 | registries := []typemux.HandlerMux{s.public, s.master} 50 | 51 | for _, mux := range registries { 52 | mux.RegisterAsync(muxrpc.Method{"manifest"}, manifest) 53 | mux.RegisterAsync(muxrpc.Method{"whoami"}, whoami) 54 | 55 | // register old room v1 commands 56 | tunnelHandler.RegisterTunnel(mux) 57 | 58 | // register new room v2 commands 59 | tunnelHandler.RegisterRoom(mux) 60 | 61 | var method = muxrpc.Method{"room"} 62 | mux.RegisterAsync(append(method, "registerAlias"), typemux.AsyncFunc(aliasHandler.Register)) 63 | mux.RegisterAsync(append(method, "revokeAlias"), typemux.AsyncFunc(aliasHandler.Revoke)) 64 | mux.RegisterAsync(append(method, "listAliases"), typemux.AsyncFunc(aliasHandler.List)) 65 | 66 | method = muxrpc.Method{"httpAuth"} 67 | mux.RegisterAsync(append(method, "invalidateAllSolutions"), typemux.AsyncFunc(siwssbHandler.InvalidateAllSolutions)) 68 | mux.RegisterAsync(append(method, "sendSolution"), typemux.AsyncFunc(siwssbHandler.SendSolution)) 69 | 70 | method = muxrpc.Method{"gossip"} 71 | mux.RegisterDuplex(append(method, "ping"), typemux.DuplexFunc(gossip.Ping)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/sbot_serv.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const Path = require('path') 6 | const tapSpec = require('tap-spec') 7 | const tape = require('tape') 8 | const { loadOrCreateSync } = require('ssb-keys') 9 | const theStack = require('secret-stack') 10 | const ssbCaps = require('ssb-caps') 11 | 12 | const testSHSappKey = bufFromEnv('TEST_APPKEY') 13 | let testAppkey = Buffer.from(ssbCaps.shs, 'base64') 14 | if (testSHSappKey !== false) { 15 | testAppkey = testSHSappKey 16 | } 17 | 18 | stackOpts = {caps: {shs: testAppkey } } 19 | let createSbot = theStack(stackOpts) 20 | .use(require('ssb-db2')) 21 | .use(require('ssb-db2/compat/db')) 22 | 23 | const testName = process.env['TEST_NAME'] 24 | const testPort = process.env['TEST_PORT'] 25 | const testSession = require(process.env['TEST_SESSIONSCRIPT']) 26 | 27 | const path = require("path") 28 | const scriptname = path.basename(__filename) 29 | 30 | // load the plugins needed for this session 31 | for (plug of testSession.secretStackPlugins) { 32 | createSbot = createSbot.use(require(plug)) 33 | } 34 | 35 | tape.createStream().pipe(tapSpec()).pipe(process.stderr); 36 | tape(testName, function (t) { 37 | function comment (msg) { 38 | t.comment(`[${scriptname}] ${msg}`) 39 | } 40 | // t.timeoutAfter(30000) // doesn't exit the process 41 | // const tapeTimeout = setTimeout(() => { 42 | // t.comment("test timeout") 43 | // process.exit(1) 44 | // }, 50000) 45 | 46 | function exit() { // call this when you're done 47 | sbot.close() 48 | comment(`closed server: ${testName}`) 49 | // clearTimeout(tapeTimeout) 50 | t.end() 51 | process.exit(0) 52 | } 53 | 54 | const tempRepo = process.env['TEST_REPO'] 55 | console.warn("my repo:", tempRepo) 56 | const keys = loadOrCreateSync(Path.join(tempRepo, 'secret')) 57 | const sbot = createSbot({ 58 | port: testPort, 59 | path: tempRepo, 60 | keys: keys, 61 | }) 62 | const alice = sbot.whoami() 63 | 64 | comment("sbot spawned, running before") 65 | 66 | function ready() { 67 | comment(`server spawned, I am: ${alice.id}`) 68 | console.log(alice.id) // tell go process who our pubkey 69 | } 70 | testSession.before(t, sbot, ready) 71 | 72 | sbot.on("rpc:connect", (remote, isClient) => { 73 | comment(`new connection: ${remote.id}`) 74 | testSession.after(t, sbot, remote, exit) 75 | }) 76 | }) 77 | 78 | // util 79 | function bufFromEnv(evname) { 80 | const has = process.env[evname] 81 | if (has) { 82 | return Buffer.from(has, 'base64') 83 | } 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /internal/broadcasts/endpoints.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package broadcasts 6 | 7 | import ( 8 | "io" 9 | "sync" 10 | 11 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/multierror" 12 | ) 13 | 14 | type EndpointsEmitter interface { 15 | Update(members []string) error 16 | io.Closer 17 | } 18 | 19 | // NewEndpointsEmitter returns the Sink, to write to the broadcaster, and the new 20 | // broadcast instance. 21 | func NewEndpointsEmitter() (EndpointsEmitter, *EndpointsBroadcast) { 22 | bcst := EndpointsBroadcast{ 23 | mu: &sync.Mutex{}, 24 | sinks: make(map[*EndpointsEmitter]struct{}), 25 | } 26 | 27 | return (*endpointsSink)(&bcst), &bcst 28 | } 29 | 30 | // EndpointsBroadcast is an interface for registering one or more Sinks to recieve 31 | // updates. 32 | type EndpointsBroadcast struct { 33 | mu *sync.Mutex 34 | sinks map[*EndpointsEmitter]struct{} 35 | } 36 | 37 | // Register a Sink for updates to be sent. also returns 38 | func (bcst *EndpointsBroadcast) Register(sink EndpointsEmitter) func() { 39 | bcst.mu.Lock() 40 | defer bcst.mu.Unlock() 41 | bcst.sinks[&sink] = struct{}{} 42 | 43 | return func() { 44 | bcst.mu.Lock() 45 | defer bcst.mu.Unlock() 46 | delete(bcst.sinks, &sink) 47 | sink.Close() 48 | } 49 | } 50 | 51 | type endpointsSink EndpointsBroadcast 52 | 53 | // Pour implements the Sink interface. 54 | func (bcst *endpointsSink) Update(members []string) error { 55 | 56 | bcst.mu.Lock() 57 | for s := range bcst.sinks { 58 | err := (*s).Update(members) 59 | if err != nil { 60 | delete(bcst.sinks, s) 61 | } 62 | } 63 | bcst.mu.Unlock() 64 | 65 | return nil 66 | } 67 | 68 | // Close implements the Sink interface. 69 | func (bcst *endpointsSink) Close() error { 70 | var sinks []EndpointsEmitter 71 | 72 | bcst.mu.Lock() 73 | defer bcst.mu.Unlock() 74 | 75 | sinks = make([]EndpointsEmitter, 0, len(bcst.sinks)) 76 | 77 | for sink := range bcst.sinks { 78 | sinks = append(sinks, *sink) 79 | } 80 | 81 | var ( 82 | wg sync.WaitGroup 83 | me multierror.List 84 | ) 85 | 86 | // might be fine without the waitgroup and concurrency 87 | 88 | wg.Add(len(sinks)) 89 | for _, sink_ := range sinks { 90 | go func(sink EndpointsEmitter) { 91 | defer wg.Done() 92 | 93 | err := sink.Close() 94 | if err != nil { 95 | me.Errs = append(me.Errs, err) 96 | return 97 | } 98 | }(sink_) 99 | } 100 | wg.Wait() 101 | 102 | if len(me.Errs) == 0 { 103 | return nil 104 | } 105 | 106 | return me 107 | } 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: Unlicense 4 | 5 | module github.com/ssbc/go-ssb-room/v2 6 | 7 | go 1.16 8 | 9 | require ( 10 | filippo.io/edwards25519 v1.0.0 // indirect 11 | github.com/BurntSushi/toml v1.3.1 12 | github.com/PuerkitoBio/goquery v1.8.1 13 | github.com/andybalholm/cascadia v1.3.2 // indirect 14 | github.com/dustin/go-humanize v1.0.1 15 | github.com/friendsofgo/errors v0.9.2 16 | github.com/go-logfmt/logfmt v0.6.0 // indirect 17 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 18 | github.com/gorilla/csrf v1.7.1 19 | github.com/gorilla/mux v1.8.0 20 | github.com/gorilla/securecookie v1.1.1 21 | github.com/gorilla/sessions v1.2.1 22 | github.com/gorilla/websocket v1.5.0 23 | github.com/hashicorp/go-multierror v1.1.1 // indirect 24 | github.com/jinzhu/now v1.1.5 // indirect 25 | github.com/mattevans/pwned-passwords v0.6.0 26 | github.com/mattn/go-sqlite3 v1.14.17 27 | github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 28 | github.com/mileusna/useragent v1.3.3 29 | github.com/nicksnyder/go-i18n/v2 v2.2.1 30 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 31 | github.com/pkg/errors v0.9.1 32 | github.com/rubenv/sql-migrate v1.4.0 33 | github.com/russross/blackfriday/v2 v2.1.0 34 | github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect 35 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 36 | github.com/spf13/cast v1.5.1 // indirect 37 | github.com/ssbc/go-muxrpc/v2 v2.0.14-0.20221111190521-10382533750c 38 | github.com/ssbc/go-netwrap v0.1.5-0.20221019160355-cd323bb2e29d 39 | github.com/ssbc/go-secretstream v1.2.11-0.20221111164233-4b41f899f844 40 | github.com/ssbc/go-ssb-refs v0.5.2 41 | github.com/stretchr/testify v1.8.4 42 | github.com/throttled/throttled/v2 v2.11.0 43 | github.com/unrolled/secure v1.13.0 44 | github.com/vcraescu/go-paginator/v2 v2.0.0 45 | github.com/volatiletech/sqlboiler/v4 v4.14.2 46 | github.com/volatiletech/strmangle v0.0.4 47 | go.cryptoscope.co/nocomment v0.0.0-20210520094614-fb744e81f810 48 | go.mindeco.de v1.12.0 49 | golang.org/x/crypto v0.9.0 50 | golang.org/x/sync v0.1.0 51 | golang.org/x/text v0.9.0 52 | golang.org/x/tools v0.6.0 53 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 54 | gorm.io/gorm v1.25.1 // indirect 55 | modernc.org/sqlite v1.23.0 // indirect 56 | ) 57 | 58 | exclude go.cryptoscope.co/ssb v0.0.0-20201207161753-31d0f24b7a79 59 | 60 | // https://github.com/rubenv/sql-migrate/pull/189 61 | // and using branch 'drop-other-drivers' for less dependency pollution (oracaldb and the like) 62 | replace github.com/rubenv/sql-migrate => github.com/cryptix/go-sql-migrate v0.0.0-20210521142015-a3e4d9974764 63 | -------------------------------------------------------------------------------- /web/handlers/set_language_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package handlers 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/ssbc/go-ssb-room/v2/web/i18n" 16 | "github.com/ssbc/go-ssb-room/v2/web/router" 17 | ) 18 | 19 | func TestLanguageDefaultNoCookie(t *testing.T) { 20 | ts := setup(t) 21 | a := assert.New(t) 22 | route := ts.URLTo(router.CompleteIndex) 23 | 24 | html, res := ts.Client.GetHTML(route) 25 | a.Equal(http.StatusOK, res.Code, "wrong HTTP status code") 26 | 27 | languageForms := html.Find("#visitor-set-language form") 28 | // two languages: english, deutsch => two
elements 29 | a.Equal(2, languageForms.Length()) 30 | 31 | // verify there is no language cookie to set yet 32 | cookieHeader := res.Header()["Set-Cookie"] 33 | for _, cookie := range cookieHeader { 34 | cookieName := strings.Split(cookie, "=")[0] 35 | a.NotEqual(cookieName, i18n.LanguageCookieName) 36 | } 37 | } 38 | 39 | func TestLanguageChooseGerman(t *testing.T) { 40 | ts := setup(t) 41 | a := assert.New(t) 42 | route := ts.URLTo(router.CompleteIndex) 43 | postEndpoint := ts.URLTo(router.CompleteSetLanguage) 44 | 45 | html, res := ts.Client.GetHTML(route) 46 | a.Equal(http.StatusOK, res.Code, "wrong HTTP status code") 47 | 48 | csrfTokenElem := html.Find(`#visitor-set-language input[name="gorilla.csrf.Token"]`) 49 | a.Equal(2, csrfTokenElem.Length()) 50 | 51 | csrfName, has := csrfTokenElem.First().Attr("name") 52 | a.True(has, "should have a name attribute") 53 | 54 | csrfValue, has := csrfTokenElem.First().Attr("value") 55 | a.True(has, "should have value attribute") 56 | 57 | // construct the post request fields, simulating picking a language 58 | setLanguageFields := url.Values{ 59 | "lang": []string{"de"}, 60 | "page": []string{"/"}, 61 | csrfName: []string{csrfValue}, 62 | } 63 | 64 | // set the referer header (important! otherwise our nicely crafted request yields a 500 :'() 65 | var refererHeader = make(http.Header) 66 | refererHeader.Set("Referer", "https://localhost") 67 | ts.Client.SetHeaders(refererHeader) 68 | 69 | // send the post request 70 | postRes := ts.Client.PostForm(postEndpoint, setLanguageFields) 71 | a.Equal(http.StatusSeeOther, postRes.Code, "wrong HTTP status code for sign in") 72 | 73 | // verify there is one language cookie to set 74 | cookieHeader := postRes.Header()["Set-Cookie"] 75 | var languageCookies int 76 | for _, cookie := range cookieHeader { 77 | cookieName := strings.Split(cookie, "=")[0] 78 | if cookieName == i18n.LanguageCookieName { 79 | languageCookies += 1 80 | } 81 | } 82 | a.Equal(1, languageCookies, "should have one language cookie set after posting") 83 | } 84 | -------------------------------------------------------------------------------- /roomdb/sqlite/migrations/01-consolidated.sql: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | -- 3 | -- SPDX-License-Identifier: CC0-1.0 4 | 5 | -- +migrate Up 6 | -- the internal users table (people who used an invite) 7 | CREATE TABLE members ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 9 | role INTEGER NOT NULL, -- member, moderator or admin 10 | pub_key TEXT NOT NULL UNIQUE, 11 | 12 | CHECK(role > 0) 13 | ); 14 | CREATE INDEX members_pubkeys ON members(pub_key); 15 | 16 | -- password login for members (in case they can't use sign-in with ssb, for whatever reason) 17 | CREATE TABLE fallback_passwords ( 18 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 19 | login TEXT NOT NULL UNIQUE, 20 | password_hash BLOB NOT NULL, 21 | 22 | member_id INTEGER NOT NULL, 23 | 24 | FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE 25 | ); 26 | CREATE INDEX fallback_passwords_by_login ON fallback_passwords(login); 27 | 28 | -- single use tokens for becoming members 29 | CREATE TABLE invites ( 30 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 31 | hashed_token TEXT UNIQUE NOT NULL, 32 | created_by INTEGER NOT NULL, 33 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 34 | 35 | active boolean NOT NULL DEFAULT TRUE, 36 | 37 | FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE 38 | ); 39 | CREATE INDEX invite_active_ids ON invites(id) WHERE active=TRUE; 40 | CREATE UNIQUE INDEX invite_active_tokens ON invites(hashed_token) WHERE active=TRUE; 41 | CREATE INDEX invite_inactive ON invites(active); 42 | 43 | -- name -> public key mappings 44 | CREATE TABLE aliases ( 45 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 46 | name TEXT UNIQUE NOT NULL, 47 | member_id INTEGER NOT NULL, 48 | signature BLOB NOT NULL, 49 | 50 | FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE 51 | ); 52 | CREATE UNIQUE INDEX aliases_ids ON aliases(id); 53 | CREATE UNIQUE INDEX aliases_names ON aliases(name); 54 | 55 | -- public keys that should never ever be let into the room 56 | CREATE TABLE denied_keys ( 57 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 58 | pub_key TEXT NOT NULL UNIQUE, 59 | comment TEXT NOT NULL, 60 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 61 | ); 62 | CREATE INDEX denied_keys_by_pubkey ON invites(active); 63 | 64 | -- +migrate Down 65 | DROP TABLE members; 66 | 67 | DROP INDEX fallback_passwords_by_login; 68 | DROP TABLE fallback_passwords; 69 | 70 | DROP INDEX invite_active_ids; 71 | DROP INDEX invite_active_tokens; 72 | DROP INDEX invite_inactive; 73 | DROP TABLE invites; 74 | 75 | DROP INDEX aliases_ids; 76 | DROP INDEX aliases_names; 77 | DROP TABLE aliases; 78 | 79 | DROP INDEX denied_keys_by_pubkey; 80 | DROP TABLE denied_keys; -------------------------------------------------------------------------------- /web/handlers/admin/aliases_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package admin 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | 14 | refs "github.com/ssbc/go-ssb-refs" 15 | "github.com/ssbc/go-ssb-room/v2/roomdb" 16 | "github.com/ssbc/go-ssb-room/v2/web/router" 17 | "github.com/ssbc/go-ssb-room/v2/web/webassert" 18 | ) 19 | 20 | func TestAliasesRevokeConfirmation(t *testing.T) { 21 | ts := newSession(t) 22 | a := assert.New(t) 23 | 24 | testKey, err := refs.ParseFeedRef("@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519") 25 | a.NoError(err) 26 | testEntry := roomdb.Alias{ID: 666, Name: "the-test-name", Feed: testKey} 27 | ts.AliasesDB.GetByIDReturns(testEntry, nil) 28 | 29 | urlRevokeConfirm := ts.URLTo(router.AdminAliasesRevokeConfirm, "id", 3) 30 | 31 | html, resp := ts.Client.GetHTML(urlRevokeConfirm) 32 | a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") 33 | 34 | a.Equal(testKey.String(), html.Find("pre#verify").Text(), "has the key for verification") 35 | 36 | form := html.Find("form#confirm") 37 | 38 | method, ok := form.Attr("method") 39 | a.True(ok, "form has method set") 40 | a.Equal("POST", method) 41 | 42 | action, ok := form.Attr("action") 43 | a.True(ok, "form has action set") 44 | 45 | addURL := ts.URLTo(router.AdminAliasesRevoke) 46 | a.Equal(addURL.String(), action) 47 | 48 | webassert.ElementsInForm(t, form, []webassert.FormElement{ 49 | {Name: "name", Type: "hidden", Value: testEntry.Name}, 50 | }) 51 | } 52 | 53 | func TestAliasesRevoke(t *testing.T) { 54 | ts := newSession(t) 55 | a := assert.New(t) 56 | 57 | urlRevoke := ts.URLTo(router.AdminAliasesRevoke) 58 | overviewURL := ts.URLTo(router.AdminMembersOverview) 59 | 60 | aliasEntry := roomdb.Alias{ 61 | ID: ts.User.ID, 62 | Feed: ts.User.PubKey, 63 | Name: "Blobby", 64 | } 65 | ts.AliasesDB.RevokeReturns(nil) 66 | ts.AliasesDB.ResolveReturns(aliasEntry, nil) 67 | 68 | addVals := url.Values{"name": []string{"the-name"}} 69 | rec := ts.Client.PostForm(urlRevoke, addVals) 70 | a.Equal(http.StatusSeeOther, rec.Code) 71 | a.Equal(overviewURL.Path, rec.Header().Get("Location")) 72 | a.True(len(rec.Result().Cookies()) > 0, "got a cookie") 73 | 74 | webassert.HasFlashMessages(t, ts.Client, overviewURL, "AdminMemberDetailsAliasRevoked") 75 | 76 | a.Equal(1, ts.AliasesDB.RevokeCallCount()) 77 | _, theName := ts.AliasesDB.RevokeArgsForCall(0) 78 | a.EqualValues("the-name", theName) 79 | 80 | // now for unknown ID 81 | ts.AliasesDB.RevokeReturns(roomdb.ErrNotFound) 82 | addVals = url.Values{"name": []string{"nope"}} 83 | rec = ts.Client.PostForm(urlRevoke, addVals) 84 | a.Equal(http.StatusSeeOther, rec.Code) 85 | a.Equal(overviewURL.Path, rec.Header().Get("Location")) 86 | a.True(len(rec.Result().Cookies()) > 0, "got a cookie") 87 | 88 | webassert.HasFlashMessages(t, ts.Client, overviewURL, "ErrorNotFound") 89 | } 90 | -------------------------------------------------------------------------------- /web/templates/auth/decide_method.tmpl: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{ define "title" }}{{i18n "AuthTitle"}}{{ end }} 8 | {{ define "content" }} 9 |
10 | {{i18n "AuthWelcome"}} 11 |
12 | 13 | 36 | {{end}} -------------------------------------------------------------------------------- /muxrpc/handlers/tunnel/server/attendants.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package server 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "sync" 12 | 13 | "github.com/ssbc/go-muxrpc/v2" 14 | refs "github.com/ssbc/go-ssb-refs" 15 | "github.com/ssbc/go-ssb-room/v2/internal/network" 16 | "github.com/ssbc/go-ssb-room/v2/roomdb" 17 | ) 18 | 19 | // AttendantsUpdate is emitted if a single member joins or leaves. 20 | // Type is either 'joined' or 'left'. 21 | type AttendantsUpdate struct { 22 | Type string `json:"type"` 23 | ID refs.FeedRef `json:"id"` 24 | } 25 | 26 | // AttendantsInitialState is emitted the first time the stream is opened 27 | type AttendantsInitialState struct { 28 | Type string `json:"type"` 29 | IDs []refs.FeedRef `json:"ids"` 30 | } 31 | 32 | func (h *Handler) attendants(ctx context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink) error { 33 | 34 | // get public key from the calling peer 35 | peer, err := network.GetFeedRefFromAddr(req.RemoteAddr()) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | pm, err := h.config.GetPrivacyMode(ctx) 41 | if err != nil { 42 | return fmt.Errorf("running with unknown privacy mode") 43 | } 44 | 45 | if pm == roomdb.ModeCommunity || pm == roomdb.ModeRestricted { 46 | _, err := h.membersdb.GetByFeed(ctx, peer) 47 | if err != nil { 48 | return fmt.Errorf("external user are not allowed to enumerate members") 49 | } 50 | } 51 | 52 | // add peer to the state 53 | h.state.AddEndpoint(peer, req.Endpoint()) 54 | 55 | // send the current state 56 | snk.SetEncoding(muxrpc.TypeJSON) 57 | err = json.NewEncoder(snk).Encode(AttendantsInitialState{ 58 | Type: "state", 59 | IDs: h.state.ListAsRefs(), 60 | }) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // register for future updates 66 | toPeer := newAttendantsEncoder(snk) 67 | h.state.RegisterAttendantsUpdates(toPeer) 68 | 69 | return nil 70 | } 71 | 72 | // a muxrpc json encoder for endpoints broadcasts 73 | type attendantsJSONEncoder struct { 74 | mu sync.Mutex // only one caller to forwarder at a time 75 | snk *muxrpc.ByteSink 76 | enc *json.Encoder 77 | } 78 | 79 | func newAttendantsEncoder(snk *muxrpc.ByteSink) *attendantsJSONEncoder { 80 | enc := json.NewEncoder(snk) 81 | snk.SetEncoding(muxrpc.TypeJSON) 82 | return &attendantsJSONEncoder{ 83 | snk: snk, 84 | enc: enc, 85 | } 86 | } 87 | 88 | func (uf *attendantsJSONEncoder) Joined(member refs.FeedRef) error { 89 | uf.mu.Lock() 90 | defer uf.mu.Unlock() 91 | return uf.enc.Encode(AttendantsUpdate{ 92 | Type: "joined", 93 | ID: member, 94 | }) 95 | } 96 | 97 | func (uf *attendantsJSONEncoder) Left(member refs.FeedRef) error { 98 | uf.mu.Lock() 99 | defer uf.mu.Unlock() 100 | return uf.enc.Encode(AttendantsUpdate{ 101 | Type: "left", 102 | ID: member, 103 | }) 104 | } 105 | 106 | func (uf *attendantsJSONEncoder) Close() error { 107 | uf.mu.Lock() 108 | defer uf.mu.Unlock() 109 | return uf.snk.Close() 110 | } 111 | -------------------------------------------------------------------------------- /docs/files/example-nginx.conf: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | server { 6 | server_name hermies.club; 7 | 8 | listen 443 ssl; # managed by Certbot 9 | 10 | ssl_certificate /etc/letsencrypt/live/hermies.club/fullchain.pem; # managed by Certbot 11 | ssl_certificate_key /etc/letsencrypt/live/hermies.club/privkey.pem; # managed by Certbot 12 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 13 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 14 | 15 | location / { 16 | proxy_pass http://localhost:8899; 17 | proxy_set_header Host $host; 18 | proxy_set_header X-Forwarded-Host $host; 19 | proxy_set_header X-Forwarded-For $remote_addr:$remote_port; 20 | proxy_set_header X-Forwarded-Proto $scheme; 21 | # for websocket 22 | proxy_http_version 1.1; 23 | proxy_set_header Upgrade $http_upgrade; 24 | # requires a $connection_upgrade definition in /etc/nginx/nginx.conf 25 | # see https://futurestud.io/tutorials/nginx-how-to-fix-unknown-connection_upgrade-variable 26 | proxy_set_header Connection $connection_upgrade; 27 | } 28 | 29 | # TODO: https://blog.tarq.io/nginx-catch-all-error-pages/ 30 | } 31 | 32 | # this server uses the (same) wildcard cert as the one above but uses a regular expression on the hostname 33 | # which extracts the first subdomain which holds the alias and forwards that to the prox_pass server 34 | server { 35 | server_name "~^(?\w+)\.hermies\.club$"; 36 | 37 | listen 443 ssl; # managed by Certbot 38 | 39 | ssl_certificate /etc/letsencrypt/live/hermies.club/fullchain.pem; # managed by Certbot 40 | ssl_certificate_key /etc/letsencrypt/live/hermies.club/privkey.pem; # managed by Certbot 41 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 42 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 43 | 44 | location = / { 45 | proxy_set_header Host $host; 46 | proxy_set_header X-Forwarded-Host $host; 47 | proxy_set_header X-Forwarded-For $remote_addr:$remote_port; 48 | proxy_set_header X-Forwarded-Proto $scheme; 49 | # "rewrite" requests with subdomains to the non-wildcard url for alias resolving 50 | # $is_args$args pass on ?encoding=json if present 51 | proxy_pass http://localhost:8899/alias/$alias$is_args$args; 52 | } 53 | 54 | location / { 55 | proxy_set_header Host $host; 56 | proxy_set_header X-Forwarded-Host $host; 57 | proxy_set_header X-Forwarded-For $remote_addr:$remote_port; 58 | proxy_set_header X-Forwarded-Proto $scheme; 59 | proxy_pass http://localhost:8899; 60 | } 61 | 62 | # TODO: https://blog.tarq.io/nginx-catch-all-error-pages/ 63 | } 64 | 65 | server { 66 | if ($host ~ hermies.club$ ) { 67 | return 301 https://$host$request_uri; 68 | } # managed by Certbot 69 | 70 | 71 | listen 80 default_server; 72 | listen [::]:80 default_server; 73 | server_name hermies.club; 74 | return 404; # managed by Certbot 75 | } 76 | -------------------------------------------------------------------------------- /internal/broadcasts/attendants.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package broadcasts 6 | 7 | import ( 8 | "io" 9 | "sync" 10 | 11 | refs "github.com/ssbc/go-ssb-refs" 12 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/multierror" 13 | ) 14 | 15 | type AttendantsEmitter interface { 16 | Joined(member refs.FeedRef) error 17 | Left(member refs.FeedRef) error 18 | 19 | io.Closer 20 | } 21 | 22 | // NewAttendantsEmitter returns the Sink, to write to the broadcaster, and the new 23 | // broadcast instance. 24 | func NewAttendantsEmitter() (AttendantsEmitter, *AttendantsBroadcast) { 25 | bcst := AttendantsBroadcast{ 26 | mu: &sync.Mutex{}, 27 | sinks: make(map[*AttendantsEmitter]struct{}), 28 | } 29 | 30 | return (*attendantsSink)(&bcst), &bcst 31 | } 32 | 33 | // AttendantsBroadcast is an interface for registering one or more Sinks to recieve 34 | // updates. 35 | type AttendantsBroadcast struct { 36 | mu *sync.Mutex 37 | sinks map[*AttendantsEmitter]struct{} 38 | } 39 | 40 | // Register a Sink for updates to be sent. also returns 41 | func (bcst *AttendantsBroadcast) Register(sink AttendantsEmitter) func() { 42 | bcst.mu.Lock() 43 | defer bcst.mu.Unlock() 44 | bcst.sinks[&sink] = struct{}{} 45 | 46 | return func() { 47 | bcst.mu.Lock() 48 | defer bcst.mu.Unlock() 49 | delete(bcst.sinks, &sink) 50 | sink.Close() 51 | } 52 | } 53 | 54 | type attendantsSink AttendantsBroadcast 55 | 56 | func (bcst *attendantsSink) Joined(member refs.FeedRef) error { 57 | bcst.mu.Lock() 58 | for s := range bcst.sinks { 59 | err := (*s).Joined(member) 60 | if err != nil { 61 | delete(bcst.sinks, s) 62 | } 63 | } 64 | bcst.mu.Unlock() 65 | 66 | return nil 67 | } 68 | 69 | func (bcst *attendantsSink) Left(member refs.FeedRef) error { 70 | bcst.mu.Lock() 71 | for s := range bcst.sinks { 72 | err := (*s).Left(member) 73 | if err != nil { 74 | delete(bcst.sinks, s) 75 | } 76 | } 77 | bcst.mu.Unlock() 78 | 79 | return nil 80 | } 81 | 82 | // Close implements the Sink interface. 83 | func (bcst *attendantsSink) Close() error { 84 | bcst.mu.Lock() 85 | defer bcst.mu.Unlock() 86 | sinks := make([]AttendantsEmitter, 0, len(bcst.sinks)) 87 | 88 | for sink := range bcst.sinks { 89 | sinks = append(sinks, *sink) 90 | } 91 | 92 | bcst.mu.Lock() 93 | defer bcst.mu.Unlock() 94 | 95 | sinks = make([]AttendantsEmitter, 0, len(bcst.sinks)) 96 | 97 | for sink := range bcst.sinks { 98 | sinks = append(sinks, *sink) 99 | } 100 | 101 | var ( 102 | wg sync.WaitGroup 103 | me multierror.List 104 | ) 105 | 106 | // might be fine without the waitgroup and concurrency 107 | 108 | wg.Add(len(sinks)) 109 | for _, sink_ := range sinks { 110 | go func(sink AttendantsEmitter) { 111 | defer wg.Done() 112 | 113 | err := sink.Close() 114 | if err != nil { 115 | me.Errs = append(me.Errs, err) 116 | return 117 | } 118 | }(sink_) 119 | } 120 | wg.Wait() 121 | 122 | if len(me.Errs) == 0 { 123 | return nil 124 | } 125 | 126 | return me 127 | } 128 | -------------------------------------------------------------------------------- /roomdb/sqlite/roomconfig.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package sqlite 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | "fmt" 11 | 12 | "github.com/ssbc/go-ssb-room/v2/roomdb" 13 | "github.com/ssbc/go-ssb-room/v2/roomdb/sqlite/models" 14 | "github.com/volatiletech/sqlboiler/v4/boil" 15 | ) 16 | 17 | var _ roomdb.RoomConfig = (*Config)(nil) 18 | 19 | // the database will only ever store one row, which contains all the room settings 20 | const configRowID = 0 21 | 22 | /* Config basically enables long-term memory for the server when it comes to storing settings. Currently, the only 23 | * stored settings is the privacy mode of the room. 24 | */ 25 | type Config struct { 26 | db *sql.DB 27 | } 28 | 29 | func (c Config) GetPrivacyMode(ctx context.Context) (roomdb.PrivacyMode, error) { 30 | config, err := models.FindConfig(ctx, c.db, configRowID) 31 | if err != nil { 32 | return roomdb.ModeUnknown, err 33 | } 34 | 35 | return config.PrivacyMode, nil 36 | } 37 | 38 | func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error { 39 | // make sure the privacy mode is an ok value 40 | err := pm.IsValid() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = transact(c.db, func(tx *sql.Tx) error { 46 | // get the settings row 47 | config, err := models.FindConfig(ctx, tx, configRowID) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // set the new privacy mode 53 | config.PrivacyMode = pm 54 | // issue update stmt 55 | rowsAffected, err := config.Update(ctx, tx, boil.Infer()) 56 | if err != nil { 57 | return err 58 | } 59 | if rowsAffected == 0 { 60 | return fmt.Errorf("setting privacy mode should have update the settings row, instead 0 rows were updated") 61 | } 62 | 63 | return nil 64 | }) 65 | 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil // alles gut!! 71 | } 72 | 73 | func (c Config) GetDefaultLanguage(ctx context.Context) (string, error) { 74 | config, err := models.FindConfig(ctx, c.db, configRowID) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | return config.DefaultLanguage, nil 80 | } 81 | 82 | func (c Config) SetDefaultLanguage(ctx context.Context, langTag string) error { 83 | if len(langTag) == 0 { 84 | return fmt.Errorf("language tag cannot be empty") 85 | } 86 | 87 | err := transact(c.db, func(tx *sql.Tx) error { 88 | // get the settings row 89 | config, err := models.FindConfig(ctx, tx, configRowID) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | // set the new language tag 95 | config.DefaultLanguage = langTag 96 | // issue update stmt 97 | rowsAffected, err := config.Update(ctx, tx, boil.Infer()) 98 | if err != nil { 99 | return err 100 | } 101 | if rowsAffected == 0 { 102 | return fmt.Errorf("setting default language should have update the settings row, instead 0 rows were updated") 103 | } 104 | 105 | return nil 106 | }) 107 | 108 | if err != nil { 109 | return err 110 | } 111 | 112 | return nil // alles gut!! 113 | } 114 | -------------------------------------------------------------------------------- /roomsrv/init_unixsock.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package roomsrv 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/ssbc/go-muxrpc/v2" 14 | kitlog "go.mindeco.de/log" 15 | "go.mindeco.de/log/level" 16 | 17 | "github.com/ssbc/go-ssb-room/v2/internal/netwraputil" 18 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 19 | ) 20 | 21 | // WithUNIXSocket enables listening for muxrpc connections on a unix socket files ($repo/socket). 22 | // This socket is not encrypted or authenticated since access to it is mediated by filesystem ownership. 23 | func WithUNIXSocket(yes bool) Option { 24 | return func(s *Server) error { 25 | s.loadUnixSock = yes 26 | return nil 27 | } 28 | } 29 | 30 | // creates the UNIX socket file listener for local usage 31 | func (s *Server) initUnixSock() error { 32 | // this races because roomsrv might not be done with init yet 33 | // TODO: refactor network peer code and make unixsock implement that (those will be inited late anyway) 34 | if s.keyPair == nil { 35 | return fmt.Errorf("roomsrv/unixsock: keypair is nil. please use unixSocket with LateOption") 36 | } 37 | spoofWrapper := netwraputil.SpoofRemoteAddress(s.keyPair.Feed.PubKey()) 38 | 39 | r := repo.New(s.repoPath) 40 | sockPath := r.GetPath("socket") 41 | 42 | // local clients (not using network package because we don't want conn limiting or advertising) 43 | c, err := net.Dial("unix", sockPath) 44 | if err == nil { 45 | c.Close() 46 | return fmt.Errorf("roomsrv: repo already in use, socket accepted connection") 47 | } 48 | os.Remove(sockPath) 49 | os.MkdirAll(filepath.Dir(sockPath), 0700) 50 | 51 | uxLis, err := net.Listen("unix", sockPath) 52 | if err != nil { 53 | return err 54 | } 55 | s.closers.Add(uxLis) 56 | 57 | go func() { 58 | 59 | acceptLoop: 60 | for { 61 | c, err := uxLis.Accept() 62 | if err != nil { 63 | if nerr, ok := err.(*net.OpError); ok { 64 | if nerr.Err.Error() == "use of closed network connection" { 65 | return 66 | } 67 | } 68 | 69 | level.Warn(s.logger).Log("event", "unix sock accept failed", "err", err) 70 | continue 71 | } 72 | 73 | wc, err := spoofWrapper(c) 74 | if err != nil { 75 | c.Close() 76 | continue 77 | } 78 | for _, w := range s.postSecureWrappers { 79 | var err error 80 | wc, err = w(wc) 81 | if err != nil { 82 | level.Warn(s.logger).Log("err", err) 83 | c.Close() 84 | continue acceptLoop 85 | } 86 | } 87 | 88 | go func(conn net.Conn) { 89 | defer conn.Close() 90 | 91 | pkr := muxrpc.NewPacker(conn) 92 | 93 | edp := muxrpc.Handle(pkr, &s.master, 94 | muxrpc.WithContext(s.rootCtx), 95 | muxrpc.WithLogger(kitlog.NewNopLogger()), 96 | ) 97 | 98 | srv := edp.(muxrpc.Server) 99 | if err := srv.Serve(); err != nil { 100 | level.Warn(s.logger).Log("conn", "serve exited", "err", err, "peer", conn.RemoteAddr()) 101 | } 102 | edp.Terminate() 103 | 104 | }(wc) 105 | } 106 | }() 107 | return nil 108 | 109 | } 110 | -------------------------------------------------------------------------------- /web/handlers/admin/aliases.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package admin 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/gorilla/csrf" 13 | "go.mindeco.de/http/render" 14 | 15 | "github.com/ssbc/go-ssb-room/v2/roomdb" 16 | weberrors "github.com/ssbc/go-ssb-room/v2/web/errors" 17 | "github.com/ssbc/go-ssb-room/v2/web/members" 18 | ) 19 | 20 | // aliasesHandler implements the managment endpoints for aliases (list and revoke), 21 | // does light validation of the web arguments and passes them through to the roomdb. 22 | type aliasesHandler struct { 23 | r *render.Renderer 24 | 25 | flashes *weberrors.FlashHelper 26 | 27 | db roomdb.AliasesService 28 | } 29 | 30 | func (h aliasesHandler) revokeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) { 31 | if req.Method != "GET" { 32 | return nil, weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected GET request")} 33 | } 34 | 35 | id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64) 36 | if err != nil { 37 | err = weberrors.ErrBadRequest{Where: "ID", Details: err} 38 | return nil, err 39 | } 40 | 41 | entry, err := h.db.GetByID(req.Context(), id) 42 | if err != nil { 43 | return nil, weberrors.ErrRedirect{ 44 | Path: redirectToMembers, 45 | Reason: err, 46 | } 47 | } 48 | 49 | return map[string]interface{}{ 50 | "Entry": entry, 51 | csrf.TemplateTag: csrf.TemplateField(req), 52 | }, nil 53 | } 54 | 55 | func (h aliasesHandler) revoke(rw http.ResponseWriter, req *http.Request) { 56 | if req.Method != "POST" { 57 | err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST request")} 58 | h.r.Error(rw, req, http.StatusMethodNotAllowed, err) 59 | return 60 | } 61 | 62 | err := req.ParseForm() 63 | if err != nil { 64 | err = weberrors.ErrRedirect{ 65 | Path: redirectToMembers, 66 | Reason: weberrors.ErrBadRequest{Where: "Form data", Details: err}, 67 | } 68 | h.r.Error(rw, req, http.StatusBadRequest, err) 69 | return 70 | } 71 | 72 | defer http.Redirect(rw, req, redirectToMembers, http.StatusSeeOther) 73 | 74 | aliasName := req.FormValue("name") 75 | 76 | ctx := req.Context() 77 | 78 | aliasEntry, err := h.db.Resolve(ctx, aliasName) 79 | if err != nil { 80 | h.flashes.AddError(rw, req, err) 81 | return 82 | } 83 | 84 | // who is doing this request 85 | currentMember := members.FromContext(ctx) 86 | if currentMember == nil { 87 | err := weberrors.ErrForbidden{Details: fmt.Errorf("not an member")} 88 | h.flashes.AddError(rw, req, err) 89 | return 90 | } 91 | 92 | // ensure own alias or admin 93 | if !aliasEntry.Feed.Equal(currentMember.PubKey) && currentMember.Role != roomdb.RoleAdmin { 94 | err := weberrors.ErrForbidden{Details: fmt.Errorf("not your alias or not an admin")} 95 | h.flashes.AddError(rw, req, err) 96 | return 97 | } 98 | 99 | err = h.db.Revoke(ctx, aliasName) 100 | if err != nil { 101 | h.flashes.AddError(rw, req, err) 102 | return 103 | } 104 | 105 | h.flashes.AddMessage(rw, req, "AdminMemberDetailsAliasRevoked") 106 | } 107 | -------------------------------------------------------------------------------- /web/errors/flashes.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package errors 6 | 7 | import ( 8 | "encoding/gob" 9 | "fmt" 10 | "html/template" 11 | "net/http" 12 | 13 | "github.com/gorilla/sessions" 14 | "github.com/ssbc/go-ssb-room/v2/web/i18n" 15 | ) 16 | 17 | type FlashHelper struct { 18 | store sessions.Store 19 | 20 | locHelper *i18n.Helper 21 | } 22 | 23 | func NewFlashHelper(s sessions.Store, loc *i18n.Helper) *FlashHelper { 24 | gob.Register(FlashMessage{}) 25 | 26 | return &FlashHelper{ 27 | store: s, 28 | locHelper: loc, 29 | } 30 | } 31 | 32 | const flashSession = "go-ssb-room-flash-messages" 33 | 34 | type FlashKind uint 35 | 36 | const ( 37 | _ FlashKind = iota 38 | // FlashError signals that a problem occured 39 | FlashError 40 | // FlashNotification represents a normal message (like "xyz added/updated successfull") 41 | FlashNotification 42 | ) 43 | 44 | type FlashMessage struct { 45 | Kind FlashKind 46 | Message template.HTML 47 | } 48 | 49 | // TODO: rethink error return - maybe panic() / maybe render package? 50 | 51 | // AddMessage expects a i18n label, translates it and adds it as a FlashNotification 52 | func (fh FlashHelper) AddMessage(rw http.ResponseWriter, req *http.Request, label string) { 53 | session, err := fh.store.Get(req, flashSession) 54 | if err != nil { 55 | panic(fmt.Errorf("flashHelper: failed to get session: %w", err)) 56 | } 57 | 58 | ih := fh.locHelper.FromRequest(req) 59 | 60 | session.AddFlash(FlashMessage{ 61 | Kind: FlashNotification, 62 | Message: ih.LocalizeSimple(label), 63 | }) 64 | 65 | if err := session.Save(req, rw); err != nil { 66 | panic(fmt.Errorf("flashHelper: failed to save session: %w", err)) 67 | } 68 | } 69 | 70 | // AddError adds a FlashError and translates the passed err using localizeError() 71 | func (fh FlashHelper) AddError(rw http.ResponseWriter, req *http.Request, err error) { 72 | session, getErr := fh.store.Get(req, flashSession) 73 | if getErr != nil { 74 | panic(fmt.Errorf("flashHelper: failed to get session: %w", err)) 75 | } 76 | 77 | ih := fh.locHelper.FromRequest(req) 78 | 79 | _, msg := localizeError(ih, err) 80 | 81 | session.AddFlash(FlashMessage{ 82 | Kind: FlashError, 83 | Message: msg, 84 | }) 85 | if err := session.Save(req, rw); err != nil { 86 | panic(fmt.Errorf("flashHelper: failed to save session: %w", err)) 87 | } 88 | } 89 | 90 | // GetAll returns all the FlashMessages, emptys and updates the store 91 | func (fh FlashHelper) GetAll(rw http.ResponseWriter, req *http.Request) ([]FlashMessage, error) { 92 | session, err := fh.store.Get(req, flashSession) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | opaqueFlashes := session.Flashes() 98 | 99 | flashes := make([]FlashMessage, len(opaqueFlashes)) 100 | 101 | for i, of := range opaqueFlashes { 102 | f, ok := of.(FlashMessage) 103 | if !ok { 104 | return nil, fmt.Errorf("GetFlashes: failed to unpack flash: %T", of) 105 | } 106 | 107 | flashes[i].Kind = f.Kind 108 | flashes[i].Message = f.Message 109 | } 110 | 111 | err = session.Save(req, rw) 112 | 113 | return flashes, err 114 | } 115 | -------------------------------------------------------------------------------- /web/handlers/admin/dashboard_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package admin 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "net/http" 11 | "testing" 12 | 13 | refs "github.com/ssbc/go-ssb-refs" 14 | "github.com/ssbc/go-ssb-room/v2/roomdb" 15 | "github.com/ssbc/go-ssb-room/v2/web/router" 16 | "github.com/ssbc/go-ssb-room/v2/web/webassert" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestDashboardSimple(t *testing.T) { 21 | ts := newSession(t) 22 | a := assert.New(t) 23 | 24 | testRef, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{0}, 32), refs.RefAlgoFeedSSB1) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | ts.RoomState.AddEndpoint(testRef, nil) // 1 online 29 | ts.MembersDB.CountReturns(4, nil) // 4 members 30 | ts.InvitesDB.CountReturns(3, nil) // 3 invites 31 | ts.DeniedKeysDB.CountReturns(2, nil) // 2 banned 32 | 33 | dashURL := ts.URLTo(router.AdminDashboard) 34 | 35 | html, resp := ts.Client.GetHTML(dashURL) 36 | a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") 37 | 38 | a.Equal("1", html.Find("#online-count").Text()) 39 | a.Equal("4", html.Find("#member-count").Text()) 40 | a.Equal("3", html.Find("#invite-count").Text()) 41 | a.Equal("2", html.Find("#denied-count").Text()) 42 | 43 | webassert.Localized(t, html, []webassert.LocalizedElement{ 44 | {"title", "AdminDashboardTitle"}, 45 | }) 46 | } 47 | 48 | // make sure the dashboard renders when someone is connected that is not a member 49 | func TestDashboardWithVisitors(t *testing.T) { 50 | ts := newSession(t) 51 | a := assert.New(t) 52 | 53 | visitorRef, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{0}, 32), refs.RefAlgoFeedSSB1) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | memberRef, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{1}, 32), refs.RefAlgoFeedSSB1) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | ts.RoomState.AddEndpoint(visitorRef, nil) 62 | ts.RoomState.AddEndpoint(memberRef, nil) 63 | 64 | ts.MembersDB.CountReturns(1, nil) 65 | // return a member for the member but not for the visitor 66 | ts.MembersDB.GetByFeedStub = func(ctx context.Context, r refs.FeedRef) (roomdb.Member, error) { 67 | if r.Equal(memberRef) { 68 | return roomdb.Member{ID: 23, Role: roomdb.RoleMember, PubKey: r}, nil 69 | } 70 | return roomdb.Member{}, roomdb.ErrNotFound 71 | } 72 | 73 | dashURL := ts.URLTo(router.AdminDashboard) 74 | 75 | html, resp := ts.Client.GetHTML(dashURL) 76 | a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") 77 | 78 | a.Equal("2", html.Find("#online-count").Text()) 79 | a.Equal("1", html.Find("#member-count").Text()) 80 | 81 | memberList := html.Find("#connected-list a") 82 | a.Equal(2, memberList.Length()) 83 | 84 | htmlVisitor := memberList.Eq(0) 85 | a.Equal(visitorRef.String(), htmlVisitor.Text()) 86 | gotLink, has := htmlVisitor.Attr("href") 87 | a.False(has, "visitor should not have a link to a details page: %v", gotLink) 88 | 89 | htmlMember := memberList.Eq(1) 90 | a.Equal(memberRef.String(), htmlMember.Text()) 91 | gotLink, has = htmlMember.Attr("href") 92 | a.True(has, "member should have a link to a details page") 93 | wantLink := ts.URLTo(router.AdminMemberDetails, "id", 23) 94 | a.Equal(wantLink.String(), gotLink) 95 | } 96 | -------------------------------------------------------------------------------- /cmd/insert-user/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // insert-user is a utility to create a new member and fallback password for them 6 | package main 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "flag" 12 | "fmt" 13 | "os" 14 | "os/user" 15 | "path/filepath" 16 | "strings" 17 | "syscall" 18 | 19 | _ "github.com/mattn/go-sqlite3" 20 | "golang.org/x/crypto/ssh/terminal" 21 | 22 | refs "github.com/ssbc/go-ssb-refs" 23 | "github.com/ssbc/go-ssb-room/v2/internal/repo" 24 | "github.com/ssbc/go-ssb-room/v2/roomdb" 25 | "github.com/ssbc/go-ssb-room/v2/roomdb/sqlite" 26 | ) 27 | 28 | func main() { 29 | u, err := user.Current() 30 | check(err) 31 | 32 | var ( 33 | role roomdb.Role = roomdb.RoleAdmin 34 | repoPath string 35 | ) 36 | 37 | flag.StringVar(&repoPath, "repo", filepath.Join(u.HomeDir, ".ssb-go-room"), "[optional] where the locally stored files of the room are located") 38 | flag.Func("role", "[optional] which role the new member should have (values: mod[erator], admin, or member. default is admin)", func(val string) error { 39 | switch strings.ToLower(val) { 40 | case "admin": 41 | role = roomdb.RoleAdmin 42 | case "mod": 43 | fallthrough 44 | case "moderator": 45 | role = roomdb.RoleModerator 46 | case "member": 47 | role = roomdb.RoleMember 48 | default: 49 | return fmt.Errorf("unknown member role: %q", val) 50 | } 51 | 52 | return nil 53 | }) 54 | flag.Parse() 55 | 56 | if _, err := os.Stat(repoPath); err != nil { 57 | if os.IsNotExist(err) { 58 | fmt.Fprintf(os.Stderr, "error: %s does not exist (-repo)?\n", repoPath) 59 | os.Exit(1) 60 | } 61 | } 62 | 63 | // we require one more argument which is not a flag. 64 | if len(flag.Args()) != 1 { 65 | cliMissingArguments("please provide a public key") 66 | } 67 | 68 | pubKey, err := refs.ParseFeedRef(flag.Arg(0)) 69 | if err != nil { 70 | fmt.Fprintln(os.Stderr, "Invalid ssb public-key reference:", err) 71 | os.Exit(1) 72 | } 73 | 74 | r := repo.New(repoPath) 75 | db, err := sqlite.Open(r) 76 | check(err) 77 | defer db.Close() 78 | 79 | fmt.Fprintln(os.Stderr, "Choose a password to be able to log into the web frontend: ") 80 | bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 81 | check(err) 82 | 83 | fmt.Fprintln(os.Stderr, "Repeat Password: ") 84 | bytePasswordRepeat, err := terminal.ReadPassword(int(syscall.Stdin)) 85 | check(err) 86 | 87 | if !bytes.Equal(bytePassword, bytePasswordRepeat) { 88 | fmt.Fprintln(os.Stderr, "Passwords didn't match") 89 | os.Exit(1) 90 | } 91 | 92 | ctx := context.Background() 93 | mid, err := db.Members.Add(ctx, pubKey, role) 94 | check(err) 95 | 96 | err = db.AuthFallback.SetPassword(ctx, mid, string(bytePassword)) 97 | check(err) 98 | 99 | fmt.Fprintf(os.Stderr, "Created member (%s) with ID %d\n", role, mid) 100 | } 101 | 102 | func cliMissingArguments(message string) { 103 | executable := strings.TrimPrefix(os.Args[0], "./") 104 | fmt.Fprintf(os.Stderr, "%s: %s\nusage:%s <@base64-encoded-public-key=.ed25519>\n", executable, message, executable) 105 | flag.Usage() 106 | os.Exit(1) 107 | } 108 | 109 | func check(err error) { 110 | if err != nil { 111 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 112 | os.Exit(1) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/repo/secret.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package repo 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | refs "github.com/ssbc/go-ssb-refs" 15 | "github.com/ssbc/go-ssb-room/v2/internal/maybemod/keys" 16 | ) 17 | 18 | func DefaultKeyPair(r Interface) (*keys.KeyPair, error) { 19 | secPath := r.GetPath("secret") 20 | keyPair, err := keys.LoadKeyPair(secPath) 21 | if err != nil { 22 | if !os.IsNotExist(err) { 23 | return nil, fmt.Errorf("repo: error opening key pair: %w", err) 24 | } 25 | keyPair, err = keys.NewKeyPair(nil) 26 | if err != nil { 27 | return nil, fmt.Errorf("repo: no keypair but couldn't create one either: %w", err) 28 | } 29 | if err := keys.SaveKeyPair(*keyPair, secPath); err != nil { 30 | return nil, fmt.Errorf("repo: error saving new identity file: %w", err) 31 | } 32 | log.Printf("saved identity %s to %s", keyPair.Feed.String(), secPath) 33 | } 34 | return keyPair, nil 35 | } 36 | 37 | func NewKeyPair(r Interface, name, algo string) (*keys.KeyPair, error) { 38 | return newKeyPair(r, name, algo, nil) 39 | } 40 | 41 | func NewKeyPairFromSeed(r Interface, name, algo string, seed io.Reader) (*keys.KeyPair, error) { 42 | return newKeyPair(r, name, algo, seed) 43 | } 44 | 45 | func newKeyPair(r Interface, name, algo string, seed io.Reader) (*keys.KeyPair, error) { 46 | var secPath string 47 | if name == "-" { 48 | secPath = r.GetPath("secret") 49 | } else { 50 | secPath = r.GetPath("secrets", name) 51 | err := os.MkdirAll(filepath.Dir(secPath), 0700) 52 | if err != nil && !os.IsExist(err) { 53 | return nil, err 54 | } 55 | } 56 | if algo != string(refs.RefAlgoFeedSSB1) && algo != string(refs.RefAlgoFeedGabby) { // enums would be nice 57 | return nil, fmt.Errorf("invalid feed refrence algo") 58 | } 59 | if _, err := keys.LoadKeyPair(secPath); err == nil { 60 | return nil, fmt.Errorf("new key-pair name already taken") 61 | } 62 | keyPair, err := keys.NewKeyPair(seed) 63 | if err != nil { 64 | return nil, fmt.Errorf("repo: no keypair but couldn't create one either: %w", err) 65 | } 66 | if err := keys.SaveKeyPair(*keyPair, secPath); err != nil { 67 | return nil, fmt.Errorf("repo: error saving new identity file: %w", err) 68 | } 69 | log.Printf("saved identity %s to %s", keyPair.Feed.String(), secPath) 70 | return keyPair, nil 71 | } 72 | 73 | func LoadKeyPair(r Interface, name string) (*keys.KeyPair, error) { 74 | secPath := r.GetPath("secrets", name) 75 | keyPair, err := keys.LoadKeyPair(secPath) 76 | if err != nil { 77 | return nil, fmt.Errorf("Load: failed to open %q: %w", secPath, err) 78 | } 79 | return keyPair, nil 80 | } 81 | 82 | func AllKeyPairs(r Interface) (map[string]*keys.KeyPair, error) { 83 | kps := make(map[string]*keys.KeyPair) 84 | err := filepath.Walk(r.GetPath("secrets"), func(path string, info os.FileInfo, err error) error { 85 | if err != nil { 86 | if os.IsNotExist(err) { 87 | return nil 88 | } 89 | return err 90 | } 91 | if info.IsDir() { 92 | return nil 93 | } 94 | if kp, err := keys.LoadKeyPair(path); err == nil { 95 | kps[filepath.Base(path)] = kp 96 | return nil 97 | } 98 | return nil 99 | }) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return kps, nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/signinwithssb/bridge.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package signinwithssb 6 | 7 | import ( 8 | "fmt" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // SignalBridge implements a way for muxrpc and http handlers to communicate about SIWSSB events 14 | type SignalBridge struct { 15 | mu *sync.Mutex 16 | 17 | sessions sessionMap 18 | } 19 | 20 | type sessionMap map[string]chan Event 21 | 22 | // Event is the unit of information that is sent over the bridge. 23 | type Event struct { 24 | Worked bool 25 | 26 | // the token value if it did work 27 | Token string 28 | 29 | // reason why it didn't work 30 | Reason error 31 | } 32 | 33 | // NewSignalBridge returns a new SignalBridge 34 | func NewSignalBridge() *SignalBridge { 35 | return &SignalBridge{ 36 | mu: new(sync.Mutex), 37 | sessions: make(sessionMap), 38 | } 39 | } 40 | 41 | // RegisterSession registers a new session on the bridge. 42 | // It returns a fresh server challenge, which acts as the session key. 43 | func (sb *SignalBridge) RegisterSession() string { 44 | sb.mu.Lock() 45 | defer sb.mu.Unlock() 46 | 47 | c := GenerateChallenge() 48 | _, used := sb.sessions[c] 49 | if used { 50 | for used { // generate new challenges until we have an un-used one 51 | c = GenerateChallenge() 52 | _, used = sb.sessions[c] 53 | } 54 | } 55 | 56 | evtCh := make(chan Event) 57 | sb.sessions[c] = evtCh 58 | 59 | go func() { // make sure the session doesn't go stale and collect dust (ie unused memory) 60 | time.Sleep(10 * time.Minute) 61 | sb.mu.Lock() 62 | defer sb.mu.Unlock() 63 | delete(sb.sessions, c) 64 | }() 65 | 66 | return c 67 | } 68 | 69 | // GetEventChannel returns the channel for the passed challenge from which future events can be read. 70 | // If sc doesn't exist, the 2nd argument is false. 71 | func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) { 72 | sb.mu.Lock() 73 | defer sb.mu.Unlock() 74 | ch, has := sb.sessions[sc] 75 | return ch, has 76 | } 77 | 78 | // SessionWorked uses the passed challenge to send on and close the open channel. 79 | // It will return an error if the session doesn't exist. 80 | func (sb *SignalBridge) SessionWorked(sc string, token string) error { 81 | return sb.sendAndClose(sc, Event{ 82 | Worked: true, 83 | Token: token, 84 | }) 85 | } 86 | 87 | // SessionFailed uses the passed challenge to send on and close the open channel. 88 | // It will return an error if the session doesn't exist. 89 | func (sb *SignalBridge) SessionFailed(sc string, reason error) error { 90 | return sb.sendAndClose(sc, Event{ 91 | Worked: false, 92 | Reason: reason, 93 | }) 94 | } 95 | 96 | func (sb *SignalBridge) sendAndClose(sc string, evt Event) error { 97 | sb.mu.Lock() 98 | defer sb.mu.Unlock() 99 | 100 | ch, ok := sb.sessions[sc] 101 | if !ok { 102 | return fmt.Errorf("no such session") 103 | } 104 | 105 | var ( 106 | err error 107 | timeout = time.NewTimer(2 * time.Minute) 108 | ) 109 | 110 | // handle what happens if the sse client isn't connected 111 | select { 112 | case <-timeout.C: 113 | err = fmt.Errorf("faled to send completed session") 114 | 115 | case ch <- evt: 116 | timeout.Stop() 117 | } 118 | 119 | // session is finalized either way 120 | close(ch) 121 | delete(sb.sessions, sc) 122 | 123 | return err 124 | } 125 | -------------------------------------------------------------------------------- /muxrpc/test/nodejs/sbot_client.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | const Path = require('path') 6 | const { loadOrCreateSync } = require('ssb-keys') 7 | const tapSpec = require("tap-spec") 8 | const tape = require('tape') 9 | const theStack = require('secret-stack') 10 | const ssbCaps = require('ssb-caps') 11 | 12 | const testSHSappKey = bufFromEnv('TEST_APPKEY') 13 | 14 | let testAppkey = Buffer.from(ssbCaps.shs, 'base64') 15 | if (testSHSappKey !== false) { 16 | testAppkey = testSHSappKey 17 | } 18 | 19 | let createSbot = theStack({caps: {shs: testAppkey } }) 20 | .use(require('ssb-db2')) 21 | .use(require('ssb-db2/compat/db')) 22 | .use(require('./testscripts/secretstack_testplugin.js')) 23 | 24 | const testName = process.env.TEST_NAME 25 | 26 | // the other peer we are talking to 27 | const testPeerAddr = process.env.TEST_PEERADDR 28 | const testPeerRef = process.env.TEST_PEERREF 29 | const testSession = require(process.env['TEST_SESSIONSCRIPT']) 30 | 31 | const path = require("path") 32 | const scriptname = path.basename(__filename) 33 | 34 | // load the plugins needed for this session 35 | for (plug of testSession.secretStackPlugins) { 36 | createSbot = createSbot.use(require(plug)) 37 | } 38 | 39 | function bufFromEnv(evname) { 40 | const has = process.env[evname] 41 | if (has) { 42 | return Buffer.from(has, 'base64') 43 | } 44 | return false 45 | } 46 | 47 | tape.createStream().pipe(tapSpec()).pipe(process.stderr) 48 | tape(testName, function (t) { 49 | function comment (msg) { 50 | t.comment(`[${scriptname}] ${msg}`) 51 | } 52 | let timeoutLength = 30000 53 | var tapeTimeout = null 54 | function ready() { // needs to be called by the before block when it's done 55 | t.timeoutAfter(timeoutLength) // doesn't exit the process 56 | tapeTimeout = setTimeout(() => { 57 | comment('!! test did not complete before timeout; shutting everything down') 58 | process.exit(1) 59 | }, timeoutLength) 60 | const to = `net:${testPeerAddr}~shs:${testPeerRef.substr(1).replace('.ed25519', '')}` 61 | comment(`dialing: ${to}`) 62 | sbot.conn.connect(to, (err, rpc) => { 63 | t.error(err, 'connected') 64 | comment(`connected to: ${rpc.id}`) 65 | testSession.after(t, sbot, rpc, exit) 66 | }) 67 | } 68 | 69 | function exit() { // call this when you're done 70 | sbot.close() 71 | comment(`closed client: ${testName}`) 72 | clearTimeout(tapeTimeout) 73 | t.end() 74 | process.exit(0) 75 | } 76 | 77 | const tempRepo = process.env['TEST_REPO'] 78 | console.warn(tempRepo) 79 | const keys = loadOrCreateSync(Path.join(tempRepo, 'secret')) 80 | const opts = { 81 | allowPrivate: true, 82 | path: tempRepo, 83 | keys: keys 84 | } 85 | 86 | opts.connections = { 87 | incoming: { 88 | tunnel: [{scope: 'public', transform: 'shs'}], 89 | }, 90 | outgoing: { 91 | net: [{transform: 'shs'}], 92 | // ws: [{transform: 'shs'}], 93 | tunnel: [{transform: 'shs'}], 94 | }, 95 | } 96 | 97 | 98 | if (testSHSappKey !== false) { 99 | opts.caps = opts.caps ? opts.caps : {} 100 | opts.caps.shs = testSHSappKey 101 | } 102 | 103 | const sbot = createSbot(opts) 104 | const alice = sbot.whoami() 105 | comment(`client spawned. I am: ${alice.id}`) 106 | 107 | console.log(alice.id) // tell go process who's incoming 108 | testSession.before(t, sbot, ready) 109 | }) 110 | -------------------------------------------------------------------------------- /muxrpc/handlers/gossip/ping.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package gossip 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "io" 11 | "time" 12 | 13 | "github.com/ssbc/go-muxrpc/v2" 14 | "go.mindeco.de/encodedTime" 15 | ) 16 | 17 | // Ping implements the server side of gossip.ping. 18 | // it's idea is mentioned here https://github.com/ssbc/ssb-gossip/#ping-duplex 19 | // and implemented by https://github.com/dominictarr/pull-ping/ 20 | func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error { 21 | type arg struct { 22 | // The only argument is the delay between two pings. 23 | // the Javascript code calls this "timeout", tho. 24 | Delay int `json:"timeout"` 25 | } 26 | 27 | var args []arg 28 | err := json.Unmarshal(req.RawArgs, &args) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // var timeout = time.Minute * 5 34 | // if len(args) == 1 { 35 | // timeout = time.Minute * time.Duration(args[0].Timeout/(60*1000)) 36 | // } 37 | 38 | // return sillyPingPong(ctx, peerSrc, peerSnk) 39 | return actualPingPong(ctx, peerSrc, peerSnk) 40 | } 41 | 42 | // actually just read and write whenever... 43 | func sillyPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error { 44 | var ( 45 | sendErr = make(chan error) 46 | receiveErr = make(chan error) 47 | ) 48 | 49 | go func() { 50 | peerSnk.SetEncoding(muxrpc.TypeJSON) 51 | enc := json.NewEncoder(peerSnk) 52 | 53 | tick := time.NewTicker(5 * time.Second) 54 | defer tick.Stop() 55 | 56 | defer close(sendErr) 57 | 58 | for { 59 | select { 60 | case <-ctx.Done(): 61 | return 62 | case <-tick.C: 63 | } 64 | 65 | var pong = encodedTime.Millisecs(time.Now()) 66 | 67 | if err := enc.Encode(pong); err != nil { 68 | sendErr <- err 69 | return 70 | } 71 | } 72 | }() 73 | 74 | go func() { 75 | defer close(receiveErr) 76 | 77 | for peerSrc.Next(ctx) { 78 | var ping encodedTime.Millisecs 79 | err := peerSrc.Reader(func(rd io.Reader) error { 80 | return json.NewDecoder(rd).Decode(&ping) 81 | }) 82 | if err != nil { 83 | receiveErr <- err 84 | return 85 | } 86 | 87 | } 88 | 89 | return 90 | }() 91 | 92 | select { 93 | case e := <-sendErr: 94 | return e 95 | case e := <-receiveErr: 96 | return e 97 | case <-ctx.Done(): 98 | return nil 99 | } 100 | } 101 | 102 | // this is how it should work, i think, but it leads to disconnects... 103 | // From the code it's hard to see but the client sends a timestamp in milliseconds (Date.now() in javascript/json) 104 | // and the other side responds with it's own timestamp. 105 | func actualPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error { 106 | peerSnk.SetEncoding(muxrpc.TypeJSON) 107 | enc := json.NewEncoder(peerSnk) 108 | 109 | for peerSrc.Next(ctx) { 110 | var ping encodedTime.Millisecs 111 | err := peerSrc.Reader(func(rd io.Reader) error { 112 | return json.NewDecoder(rd).Decode(&ping) 113 | }) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | //when := time.Time(ping) 119 | //fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when)) 120 | 121 | pong := encodedTime.Millisecs(time.Now()) 122 | err = enc.Encode(pong) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // time.Sleep(timeout) 128 | } 129 | 130 | return peerSrc.Err() 131 | } 132 | --------------------------------------------------------------------------------