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>