update readme and build

This commit is contained in:
2026-05-24 07:18:54 +00:00
parent 45e18b5423
commit 5b4803bc49
9 changed files with 447 additions and 38 deletions
+79 -9
View File
@@ -18,6 +18,9 @@ import (
//go:embed web/shell.html
var shellPageHTML string
//go:embed web/login.html
var loginPageHTML string
//go:embed web/favicon.svg
var faviconSVG string
@@ -57,24 +60,68 @@ func handleStaticJS(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
// handleIndex: always creates a fresh workspace and redirects to its stable URL.
// PTY sessions are started lazily by the frontend via WebSocket connections.
// handleLogin serves the standalone login page (GET) or redirects authed users.
func handleLogin(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/login" {
http.NotFound(w, r)
return
}
if isAuthed(r) {
http.Redirect(w, r, "/", http.StatusFound)
return
}
next := r.URL.Query().Get("next")
if !isValidNext(next) {
next = "/"
}
tok := setCSRFCookie(w)
html := strings.NewReplacer(
"[[CSRF_TOKEN]]", tok,
"[[NEXT]]", next,
).Replace(loginPageHTML)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.Write([]byte(html)) //nolint:errcheck
}
// isValidNext rejects open-redirect targets; only "/" and "/s/<hex>" allowed.
func isValidNext(next string) bool {
if next == "" || next == "/" {
return true
}
if strings.HasPrefix(next, "/s/") {
return validID(strings.TrimPrefix(next, "/s/"))
}
return false
}
// handleIndex: creates a fresh workspace and redirects to its stable URL.
// Unauthenticated requests are sent to the login page first.
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
if !isAuthed(r) {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
http.Redirect(w, r, "/s/"+randHex(16), http.StatusFound)
}
// handleShell: serves the terminal page for an existing (or new) workspace ID.
// handleShell: serves the terminal page for a workspace ID.
// Unauthenticated requests are redirected to /login?next=...
func handleShell(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/s/")
if !validID(id) {
http.NotFound(w, r)
return
}
serveTerminalPage(w, id, isAuthed(r))
if !isAuthed(r) {
http.Redirect(w, r, "/login?next=/s/"+id, http.StatusFound)
return
}
serveTerminalPage(w, id, true)
}
func serveTerminalPage(w http.ResponseWriter, workspaceID string, authed bool) {
@@ -94,15 +141,36 @@ func handleAuth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte(`{"error":"POST only"}`))
w.Write([]byte(`{"error":"POST only"}`)) //nolint:errcheck
return
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"bad form"}`))
w.Write([]byte(`{"error":"bad form"}`)) //nolint:errcheck
return
}
if checkCreds(strings.TrimSpace(r.FormValue("username")), r.FormValue("password")) {
// CSRF validation (skipped in -nopw mode which never shows the login page)
if !nopwMode && !checkCSRF(r) {
logAuthAttempt(r, "", false, "csrf_invalid")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"invalid request"}`)) //nolint:errcheck
return
}
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
// Input bounds — reject obviously bad values before touching the hasher
if len(username) == 0 || len(username) > 64 || len(password) == 0 || len(password) > 1024 {
time.Sleep(500 * time.Millisecond)
logAuthAttempt(r, username, false, "invalid_input")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"invalid input"}`)) //nolint:errcheck
return
}
if checkCreds(username, password) {
http.SetCookie(w, &http.Cookie{
Name: authCookieName,
Value: makeAuthToken(),
@@ -112,11 +180,13 @@ func handleAuth(w http.ResponseWriter, r *http.Request) {
SameSite: http.SameSiteLaxMode,
MaxAge: int(authTokenTTL.Seconds()),
})
w.Write([]byte(`{"ok":true}`))
logAuthAttempt(r, username, true, "login_success")
w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
} else {
time.Sleep(500 * time.Millisecond) // blunt brute-force deterrent
logAuthAttempt(r, username, false, "invalid_credentials")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"Invalid username or password"}`))
w.Write([]byte(`{"error":"Invalid username or password"}`)) //nolint:errcheck
}
}