Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
package web
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/clientip"
|
|
)
|
|
|
|
// LandingData is what the landing page shows. It is deliberately almost
|
|
// nothing: this page is served without a key, to anyone who can reach the
|
|
// address.
|
|
type LandingData struct {
|
|
BaseURL string
|
|
AuthEnabled bool
|
|
SignupOpen bool
|
|
Contact string
|
|
ContactHref string
|
|
// ContactHTML is the contact as the page shows it, a link when it is
|
|
// one, for the sentence that names it to place.
|
|
ContactHTML template.HTML
|
|
Notice string
|
|
Version string
|
|
Now string
|
|
}
|
|
|
|
// Landing serves the page at "/" on the client-facing port.
|
|
//
|
|
// Without it that path is a 404, which is what someone sees when they paste the
|
|
// address into a browser — an endpoint that looks broken rather than one that
|
|
// tells them how to use it.
|
|
//
|
|
// It lists no models and no cluster state. Everything that says what is here
|
|
// stays behind the key, including /v1/models.
|
|
func Landing(version string, authEnabled, signupOpen bool, contact, notice string) http.HandlerFunc {
|
|
text, href := contactLink(contact)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Only the root: a catch-all would turn every mistyped path into this
|
|
// page, hiding genuine 404s from anyone debugging a client.
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
render(w, r, "landing", LandingData{
|
|
BaseURL: baseURL(r),
|
|
AuthEnabled: authEnabled,
|
|
SignupOpen: signupOpen,
|
|
Contact: text,
|
|
ContactHref: href,
|
|
ContactHTML: contactMarkup(text, href),
|
|
Notice: notice,
|
|
Version: version,
|
|
Now: time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
}
|
|
|
|
// baseURL reconstructs the address the client actually used.
|
|
//
|
|
// Behind a reverse proxy the scheme and host this process sees are the internal
|
|
// ones, so the forwarded headers are what make the copy-pasteable example on
|
|
// the page correct. They are attacker-controlled when nothing sets them, which
|
|
// is why they are only reflected into a displayed URL and never used for a
|
|
// decision.
|
|
func baseURL(r *http.Request) string {
|
|
// What a trusted proxy says it was asked for, else what this server was:
|
|
// the address goes into scripts clients pipe to a shell, so it is not
|
|
// taken from whoever sends a header.
|
|
scheme, host := clientip.Proto(r), clientip.Host(r)
|
|
if host == "" {
|
|
host = "localhost:8080"
|
|
}
|
|
return scheme + "://" + host
|
|
}
|
|
|
|
// firstValue takes the first entry of a comma-separated forwarded header: a
|
|
// chain of proxies appends rather than replaces.
|
|
func firstValue(v string) string {
|
|
if i := strings.IndexByte(v, ','); i >= 0 {
|
|
v = v[:i]
|
|
}
|
|
return strings.TrimSpace(v)
|
|
}
|