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