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)
}