Initial commit: DSH Web Manager

This commit is contained in:
2026-10-13 09:34:47 +08:00
commit 85332bbc56
135 changed files with 19712 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
package api
import (
"bytes"
"context"
"encoding/binary"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"testing/iotest"
"git.misaka.ren/m1saka/dsh-web/internal/docker"
"git.misaka.ren/m1saka/dsh-web/internal/store"
)
func runtimeTestFrame(payload []byte) []byte {
var h [8]byte
h[0] = 1
binary.BigEndian.PutUint32(h[4:], uint32(len(payload)))
return append(h[:], payload...)
}
func TestRuntimeTokenFromStreamLargeRawAndFramed(t *testing.T) {
for _, tc := range []struct {
name string
data []byte
token string
}{
{"raw", append(bytes.Repeat([]byte("x"), 9<<20), []byte("\ndsh web: http://runtime/?token=raw-tail\n")...), "raw-tail"},
{"framed", runtimeTestFrame(append(bytes.Repeat([]byte("x"), 9<<20), []byte("\ndsh web: http://runtime/?token=framed-tail\n")...)), "framed-tail"},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := runtimeTokenFromStream(bytes.NewReader(tc.data))
if err != nil || got != tc.token {
t.Fatalf("token=%q err=%v", got, err)
}
})
}
}
func TestRuntimeTokenFromStreamSplitLatestAndTruncatedFrame(t *testing.T) {
data := []byte("dsh web: http://runtime/?token=first-token\ndsh web: http://runtime/?token=second-token\n")
got, err := runtimeTokenFromStream(iotest.OneByteReader(bytes.NewReader(data)))
if err != nil || got != "second-token" {
t.Fatalf("token=%q err=%v", got, err)
}
framed := runtimeTestFrame([]byte("dsh web: http://runtime/?token=truncated"))
framed = framed[:len(framed)-2]
if _, err := runtimeTokenFromStream(bytes.NewReader(framed)); err == nil {
t.Fatal("truncated frame accepted")
}
}
func TestRuntimeCookiePersistenceAndHostExchange(t *testing.T) {
var mu sync.Mutex
logs, exchanges := 0, 0
var seen []string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
if r.URL.Path == "/" {
exchanges++
token := r.URL.Query().Get("token")
seen = append(seen, token)
w.Header().Set("Set-Cookie", "session="+token+"-"+r.Host+"; Max-Age=60")
w.WriteHeader(http.StatusSeeOther)
return
}
if strings.HasSuffix(r.URL.Path, "/logs") {
logs++
io.WriteString(w, "dsh web: http://runtime/?token=persist-token\n")
return
}
w.WriteHeader(http.StatusNoContent)
}))
defer upstream.Close()
t.Setenv("DOCKER_HOST", upstream.URL)
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {})
a.docker = docker.New(upstream.URL)
i := store.Instance{ID: "persist", UID: "owner", ContainerID: "container"}
if err := a.s.AddInstance(i); err != nil {
t.Fatal(err)
}
target, _ := url.Parse(upstream.URL)
ctx := context.Background()
if got, err := a.runtimeCookie(ctx, i, "2025-01-01T00:00:00Z", target, "panel.test"); err != nil || got != "session=persist-token-panel.test" {
t.Fatalf("first cookie=%q err=%v", got, err)
}
b := New(a.s, a.root)
b.docker = docker.New(upstream.URL)
if got, err := b.runtimeCookie(ctx, i, "2025-01-01T00:00:00Z", target, "panel.test"); err != nil || got != "session=persist-token-panel.test" {
t.Fatalf("persisted cookie=%q err=%v", got, err)
}
if got, err := b.runtimeCookie(ctx, i, "2025-01-01T00:00:00Z", target, "localhost"); err != nil || got != "session=persist-token-localhost" {
t.Fatalf("localhost cookie=%q err=%v", got, err)
}
if got, err := b.runtimeCookie(ctx, i, "2025-01-01T00:00:00Z", target, "panel.test"); err != nil || got != "session=persist-token-panel.test" {
t.Fatalf("host cookie=%q err=%v", got, err)
}
proxy := runtimeReverseProxy(target, "/dsh/persist", "panel.test", "session=persist-token-panel.test")
b.invalidateRejectedRuntimeCookie(proxy, i, "2025-01-01T00:00:00Z", "panel.test", "session=persist-token-panel.test")
if err := proxy.ModifyResponse(&http.Response{StatusCode: 401, Header: make(http.Header), Body: io.NopCloser(strings.NewReader("denied"))}); err != nil {
t.Fatal(err)
}
if got, err := b.runtimeCookie(ctx, i, "2025-01-01T00:00:00Z", target, "panel.test"); err != nil || got != "session=persist-token-panel.test" {
t.Fatalf("reexchange: %q %v", got, err)
}
if _, err := b.runtimeCookie(ctx, i, "2025-01-02T00:00:00Z", target, "panel.test"); err != nil {
t.Fatalf("changed start time: %v", err)
}
mu.Lock()
defer mu.Unlock()
if logs != 2 {
t.Fatalf("log reads=%d, want 2", logs)
}
if exchanges != 4 {
t.Fatalf("exchanges=%d, want 4", exchanges)
}
for _, token := range seen {
if token != "persist-token" {
t.Fatalf("exchanged token=%q", token)
}
}
}
func TestRuntimeAuthDiskEncryptedAndTamperRejected(t *testing.T) {
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {})
entry := runtimeAuthEntry{Container: "container", Started: "2025-01-01T00:00:00Z", Token: "super-secret-token", Host: "panel.test", Cookies: "session=super-secret-token"}
key := entry.Container + "/" + entry.Started
if err := a.saveRuntimeAuth("instance", key, entry); err != nil {
t.Fatal(err)
}
path := a.runtimeAuthPath("instance", key)
st, err := os.Stat(path)
if err != nil || st.Mode().Perm() != 0600 {
t.Fatalf("stat=%v mode=%v", err, st.Mode().Perm())
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(data, []byte(entry.Token)) || bytes.Contains(data, []byte(entry.Cookies)) {
t.Fatal("runtime token stored in plaintext")
}
data[len(data)/2] ^= 1
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
if _, ok := a.loadRuntimeAuth("instance", key); ok {
t.Fatal("tampered runtime auth accepted")
}
}
func TestRuntimeCookieSaveFailureDoesNotCache(t *testing.T) {
var mu sync.Mutex
logs := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/logs") {
mu.Lock()
logs++
mu.Unlock()
io.WriteString(w, "dsh web: http://runtime/?token=save-failure-token\n")
return
}
w.Header().Set("Set-Cookie", "session=save-failure; Max-Age=60")
w.WriteHeader(http.StatusSeeOther)
}))
defer upstream.Close()
t.Setenv("DOCKER_HOST", upstream.URL)
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {})
badRoot := filepath.Join(t.TempDir(), "not-a-directory")
a.docker = docker.New(upstream.URL)
if err := os.WriteFile(badRoot, []byte("x"), 0600); err != nil {
t.Fatal(err)
}
a.root = badRoot
i := store.Instance{ID: "save-failure", UID: "owner", ContainerID: "container"}
target, _ := url.Parse(upstream.URL)
for n := 0; n < 2; n++ {
if _, err := a.runtimeCookie(context.Background(), i, "2025-01-01T00:00:00Z", target, "panel.test"); err == nil {
t.Fatal("save failure accepted")
}
}
mu.Lock()
defer mu.Unlock()
if logs != 2 {
t.Fatalf("log reads=%d after save failures, want 2", logs)
}
}