10 |
{{i18n "NoticeList"}}
13 |
14 |
{{i18n "NoticeListWelcome"}}
15 |
16 | {{ template "flashes" . }}
17 |
18 | {{range .AllNotices}}
19 |
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 | 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 |
Waiting for confirmation
19 |
20 |
{{i18n "AuthWithSSBError"}}
21 |
22 | {{if not .IsSolvingRemotely}}
23 |
24 |
25 |
{{i18n "AuthWithSSBInstructQR"}}
26 |
27 |

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 |
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 |
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 |
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 | [](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 | 
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 |
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 |
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