Files
llmgw/internal/web/contact.go
T
Ilker ManapandClaude Opus 5 8d35faf6f8 Give the landing page somewhere to send people
The page explained the API, said keys come from an operator, and left the
reader there. Someone who reaches the address without a key had no route to
getting one, which is where onboarding stopped.

`landing.contact` is that route, because the gateway cannot guess who to ask.
An address becomes a mailto link, an http(s) URL stays a link, and anything
else — a channel, a queue, a person — is shown as text. Other schemes are
never linked: this string comes from configuration, configuration is not
always written by whoever runs the gateway, and a javascript: href on a page
served to every visitor is a short path to a stored XSS. Escaping the text
does not make a scheme safe.

Unconfigured, it now says something rather than nothing, and the page explains
that a key arrives as a one-time link so the reader knows what to expect
before it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 09:22:42 +02:00

48 lines
1.4 KiB
Go

package web
import (
"net/url"
"strings"
)
// contactLink decides how to render the configured contact.
//
// An address becomes a mailto:, an http(s) URL stays a link, and anything else
// — a channel name, an internal ticket queue, a person — is shown as text.
//
// The scheme allowlist is the point. This string comes from configuration, but
// configuration is not always written by the person who runs the gateway, and a
// javascript: or data: href on a page served to every visitor is a stored XSS
// with a very short path to it. Templates escape the text; they do not make a
// scheme safe.
func contactLink(contact string) (text, href string) {
c := strings.TrimSpace(contact)
if c == "" {
return "", ""
}
if u, err := url.Parse(c); err == nil {
switch strings.ToLower(u.Scheme) {
case "http", "https", "mailto":
return c, c
case "":
// No scheme. A bare address is the common case and worth linking.
if looksLikeEmail(c) {
return c, "mailto:" + c
}
}
}
return c, ""
}
// looksLikeEmail is deliberately crude: it decides whether to add a mailto:,
// and being wrong means a link that does not open a mail client rather than
// anything worse.
func looksLikeEmail(s string) bool {
at := strings.IndexByte(s, '@')
if at <= 0 || at == len(s)-1 || strings.ContainsAny(s, " \t/") {
return false
}
return strings.Contains(s[at+1:], ".")
}