1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
diff --git a/src/server/logic/allowed_requesters.go b/src/server/logic/allowed_requesters.go
new file mode 100644
index 0000000..be21688
--- /dev/null
+++ b/src/server/logic/allowed_requesters.go
@@ -0,0 +1,56 @@
+package logic
+
+import (
+ "bufio"
+ "os"
+ "strings"
+)
+
+// isRequesterAllowed reports whether the given fediverse moniker (@user@host)
+// is permitted to request new feeds.
+//
+// Policy:
+// - If allowedFile is empty (no path configured), everyone is allowed.
+// - If the file does not exist or contains no usable entries, everyone is
+// allowed (fail-open, matching the "no allowlist configured" case).
+// - Otherwise only monikers listed in the file (one per line) are allowed.
+//
+// Matching is case-insensitive. Blank lines and lines starting with '#' are
+// ignored. A leading '@' on list entries is optional.
+func isRequesterAllowed(allowedFile, moniker string) bool {
+ if strings.TrimSpace(allowedFile) == "" {
+ return true
+ }
+ f, err := os.Open(allowedFile)
+ if err != nil {
+ // No allowlist file → treat as unconfigured (open).
+ return true
+ }
+ defer f.Close()
+
+ want := normaliseMoniker(moniker)
+ any := false
+ sc := bufio.NewScanner(f)
+ sc.Split(bufio.ScanLines)
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ any = true
+ if normaliseMoniker(line) == want {
+ return true
+ }
+ }
+ // If the file had no usable entries, treat as unconfigured (open).
+ if !any {
+ return true
+ }
+ return false
+}
+
+func normaliseMoniker(s string) string {
+ s = strings.ToLower(strings.TrimSpace(s))
+ s = strings.TrimPrefix(s, "@")
+ return s
+}
diff --git a/src/server/logic/inbox.go b/src/server/logic/inbox.go
index a745f2c..9dfee3e 100644
--- a/src/server/logic/inbox.go
+++ b/src/server/logic/inbox.go
@@ -377,6 +377,18 @@ func (ib *inbox) HandleCreateNote(
// What goes into to and cc
to, cc := ib.getRecipients(act.Actor, senderInfo.Followers, !toPublicOrFollowers)
+ // Allowlist: if configured, only permitted requesters may add new feeds.
+ if !isRequesterAllowed(ib.cfg.AllowedRequestersFile, moniker) {
+ ib.logger.Infof("Requester %s is not on the allowlist; refusing feed request", moniker)
+ msg := ib.txt.WithVals("reply_not_allowed.html", map[string]string{
+ "moniker": moniker,
+ "userUrl": senderInfo.Id,
+ })
+ ib.messenger.SendMessageAsync(ib.cfg.Birb.User, senderInfo.Inbox, msg,
+ []*MsgMention{{moniker, act.Actor}}, to, cc, act.Object.Id)
+ return
+ }
+
// Look for exactly 1 valid URL in message
blogUrl := ib.getUrl(act.Object.Content)
if blogUrl == "" {
diff --git a/src/server/shared/config.go b/src/server/shared/config.go
index be4b7bf..274b4eb 100644
--- a/src/server/shared/config.go
+++ b/src/server/shared/config.go
@@ -23,6 +23,11 @@ type Config struct {
Host string `json:"host"`
DbFile string `json:"db_file"`
BlockedFeedsFile string `json:"blocked_feeds_file"`
+ // AllowedRequestersFile, if set to a non-empty path pointing at a non-empty
+ // file, restricts who may request new feeds: only fediverse monikers
+ // (@user@host) listed in the file (one per line) are allowed. If unset or the
+ // file is empty, anyone may request feeds (original behaviour).
+ AllowedRequestersFile string `json:"allowed_requesters_file"`
ProfileDir string `json:"profile_dir"`
ProfileKeepDays int `json:"profile_keep_days"`
CachePageTemplates bool `json:"cache_page_templates"`
diff --git a/src/server/texts/snippets/reply_not_allowed.html b/src/server/texts/snippets/reply_not_allowed.html
new file mode 100644
index 0000000..fe7f246
--- /dev/null
+++ b/src/server/texts/snippets/reply_not_allowed.html
@@ -0,0 +1 @@
+<p><span class="h-card" translate="no"><a href="{{userUrl}}" class="u-url mention">{{moniker}}</a></span> Sorry; this instance only accepts feed requests from allowed accounts.</p>
|