mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Free per-user homepages at <name>.<host> (wildcard subdomains)
- sites.AskHandler now approves <name>.AGENTBBS_HOST on-demand-TLS when <name>
is a registered member (so only real users mint subdomain certs).
- Caddyfile: *.${DOMAIN} block serving users/<name>/public_html with on_demand
TLS; unknown users 404. Needs a wildcard DNS record *.${DOMAIN} -> host.
- Test: TestAskUserSubdomain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
74c292f341
commit
ae8b1c25f4
3 changed files with 73 additions and 0 deletions
|
|
@ -48,6 +48,7 @@ type Manager struct {
|
||||||
st store.Store
|
st store.Store
|
||||||
usersDir string // <data>/users
|
usersDir string // <data>/users
|
||||||
domDir string // <data>/domains (the symlink farm Caddy serves)
|
domDir string // <data>/domains (the symlink farm Caddy serves)
|
||||||
|
baseHost string // AGENTBBS_HOST, e.g. bbs.profullstack.com (for <name>.<host>)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager prepares the symlink farm under dataDir/domains.
|
// NewManager prepares the symlink farm under dataDir/domains.
|
||||||
|
|
@ -60,6 +61,7 @@ func NewManager(st store.Store, dataDir string) (*Manager, error) {
|
||||||
st: st,
|
st: st,
|
||||||
usersDir: filepath.Join(dataDir, "users"),
|
usersDir: filepath.Join(dataDir, "users"),
|
||||||
domDir: domDir,
|
domDir: domDir,
|
||||||
|
baseHost: strings.ToLower(strings.TrimSpace(os.Getenv("AGENTBBS_HOST"))),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,6 +134,20 @@ func (m *Manager) AskHandler() http.Handler {
|
||||||
http.Error(w, "bad domain", http.StatusBadRequest)
|
http.Error(w, "bad domain", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Free user-homepage subdomain: <name>.<baseHost> for any registered
|
||||||
|
// member (e.g. alice.bbs.profullstack.com). Single label only.
|
||||||
|
if m.baseHost != "" && strings.HasSuffix(d, "."+m.baseHost) {
|
||||||
|
label := strings.TrimSuffix(d, "."+m.baseHost)
|
||||||
|
if label != "" && !strings.Contains(label, ".") {
|
||||||
|
if _, ok, err := m.st.UserByName(label); err != nil {
|
||||||
|
http.Error(w, "lookup error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, ok, err := m.st.DomainUser(d); err != nil {
|
if _, ok, err := m.st.DomainUser(d); err != nil {
|
||||||
http.Error(w, "lookup error", http.StatusInternalServerError)
|
http.Error(w, "lookup error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -110,3 +110,39 @@ func TestManagerAddRemoveSyncAsk(t *testing.T) {
|
||||||
t.Error("expected domain unmapped in store")
|
t.Error("expected domain unmapped in store")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAskUserSubdomain(t *testing.T) {
|
||||||
|
t.Setenv("AGENTBBS_HOST", "bbs.profullstack.com")
|
||||||
|
dir := t.TempDir()
|
||||||
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer st.Close()
|
||||||
|
if _, err := st.EnsureUser("alice", "member", "SHA256:aaa"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := NewManager(st, dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := m.AskHandler()
|
||||||
|
|
||||||
|
for _, c := range []struct {
|
||||||
|
domain string
|
||||||
|
code int
|
||||||
|
}{
|
||||||
|
{"alice.bbs.profullstack.com", http.StatusOK}, // registered member → cert allowed
|
||||||
|
{"nobody.bbs.profullstack.com", http.StatusNotFound}, // no such user
|
||||||
|
{"a.b.bbs.profullstack.com", http.StatusNotFound}, // multi-label, not a user subdomain
|
||||||
|
{"bbs.profullstack.com", http.StatusNotFound}, // apex is not a user subdomain
|
||||||
|
} {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/check?domain="+c.domain, nil)
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != c.code {
|
||||||
|
t.Errorf("ask %q = %d, want %d", c.domain, rec.Code, c.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
21
setup.sh
21
setup.sh
|
|
@ -381,6 +381,7 @@ fi
|
||||||
# Let Caddy (user 'caddy') read the per-user public_html trees.
|
# Let Caddy (user 'caddy') read the per-user public_html trees.
|
||||||
usermod -aG "$SVC_USER" caddy 2>/dev/null || true
|
usermod -aG "$SVC_USER" caddy 2>/dev/null || true
|
||||||
log "writing Caddyfile"
|
log "writing Caddyfile"
|
||||||
|
DOMAIN_RE=$(printf '%s' "$DOMAIN" | sed 's/[.]/\\./g') # dots escaped for Caddy host_regexp
|
||||||
cat > /etc/caddy/Caddyfile <<CADDY
|
cat > /etc/caddy/Caddyfile <<CADDY
|
||||||
{
|
{
|
||||||
email ${ACME_EMAIL}
|
email ${ACME_EMAIL}
|
||||||
|
|
@ -417,6 +418,26 @@ ${DOMAIN} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Free per-user homepages at <name>.${DOMAIN} (needs wildcard DNS
|
||||||
|
# *.${DOMAIN} -> this host). On-demand TLS mints a cert only when agentbbs's
|
||||||
|
# ask endpoint confirms <name> is a registered member, so random subdomains
|
||||||
|
# can't trigger certificate issuance.
|
||||||
|
*.${DOMAIN} {
|
||||||
|
encode zstd gzip
|
||||||
|
tls {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
@user host_regexp user ^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.${DOMAIN_RE}\$
|
||||||
|
handle @user {
|
||||||
|
root * ${DATA_DIR}/users/{re.user.1}/public_html
|
||||||
|
try_files {path} {path}/index.html
|
||||||
|
file_server browse
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
respond "no such user" 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Custom domains a member pointed at this host (ssh domain@${DOMAIN} add ...).
|
# Custom domains a member pointed at this host (ssh domain@${DOMAIN} add ...).
|
||||||
# The symlink farm in domains/ maps each host to its owner's public_html, so
|
# The symlink farm in domains/ maps each host to its owner's public_html, so
|
||||||
# {host} resolves to the right tree; unmapped hosts 404 (and never got a cert).
|
# {host} resolves to the right tree; unmapped hosts 404 (and never got a cert).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue