package cudy import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) func clientRow(id, mac, band, signal string) string { return fmt.Sprintf(`

PRIVATE NAME
%s

192.168.10.9
%s

192.168.10.9
%s

%s

%s

`, id, id, band, id, mac, mac, id, signal, signal) } func clientTable(rows string) string { return `` + rows + `
IP / MAC Address
` } func TestParseClients(t *testing.T) { rows := clientRow("1", "AA:BB:CC:DD:EE:02", "2.4G WiFi", "-60 dBm") + clientRow("2", "AA:BB:CC:DD:EE:01", "5G WiFi", "—") + clientRow("3", "AA:BB:CC:DD:EE:03", "Ethernet", "—") clients, err := parseClients([]byte(clientTable(rows))) if err != nil || len(clients) != 2 { t.Fatalf("reading: %v, count %d", err, len(clients)) } if clients[0].MAC != "AA:BB:CC:DD:EE:01" || clients[0].Signal != nil || clients[1].Signal == nil || *clients[1].Signal != -60 { t.Fatalf("unexpected snapshot: %+v", clients) } data, _ := json.Marshal(clients) if strings.Contains(string(data), "PRIVATE") { t.Fatal("hostname leaked") } for _, text := range []string{`
Login
`, clientTable(clientRow("1", "bad", "5G WiFi", "-60 dBm")), clientTable(strings.ReplaceAll(clientRow("1", "AA:BB:CC:DD:EE:01", "5G WiFi", "-60 dBm"), "cbi-table-", "changed-")), clientTable(clientRow("1", "AA:BB:CC:DD:EE:01", "5 GHz", "-60 dBm")), clientTable(clientRow("1", "AA:BB:CC:DD:EE:01", "", "-60 dBm")), clientTable(rows + clientRow("4", "AA:BB:CC:DD:EE:01", "5G WiFi", "-70 dBm"))} { if _, err := parseClients([]byte(text)); !errors.Is(err, ErrRead) { t.Fatalf("invalid DOM accepted: %v", err) } } clients, err = parseClients([]byte(clientTable(""))) if err != nil || len(clients) != 0 { t.Fatal("empty table rejected") } } func TestReaderSessionAndSafeErrors(t *testing.T) { sessions := 0 expire := false mux := http.NewServeMux() mux.HandleFunc("/cgi-bin/luci/", func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { if err := r.ParseForm(); err != nil { t.Error(err) } first := sha256.Sum256([]byte("private-password" + "salt")) want := sha256.Sum256([]byte(hex.EncodeToString(first[:]) + "token")) if r.Form.Get("luci_password") != hex.EncodeToString(want[:]) || r.Form.Get("_csrf") != "csrf" || r.Form.Get("luci_language") != "en" { t.Error("incorrect login") } sessions++ http.SetCookie(w, &http.Cookie{Name: "sysauth", Value: "session", Path: "/"}) fmt.Fprint(w, "authenticated") return } w.WriteHeader(403) fmt.Fprint(w, `
`) }) mux.HandleFunc("/cgi-bin/luci/admin/get_token", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Error("token method") } fmt.Fprint(w, "token") }) mux.HandleFunc("/cgi-bin/luci/admin/network/devices/devlist", func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" || r.URL.RawQuery != "detail=1" { t.Error("not a read-only request") } if _, err := r.Cookie("sysauth"); err != nil { t.Error("session cookie missing") } if expire { expire = false w.WriteHeader(403) return } fmt.Fprint(w, clientTable(clientRow("1", "AA:BB:CC:DD:EE:01", "5G WiFi", "-55 dBm"))) }) server := httptest.NewServer(mux) defer server.Close() reader, err := NewReader(server.URL, "private-password") if err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { if _, err := reader.Read(context.Background()); err != nil { t.Fatal(err) } } if sessions != 1 { t.Fatal("session not reused") } expire = true if _, err := reader.Read(context.Background()); err != nil { t.Fatal(err) } if sessions != 2 { t.Fatal("expired session not refreshed once") } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := reader.Read(ctx); !errors.Is(err, context.Canceled) { t.Fatal("cancellation lost") } for _, base := range []string{"https://example.com", "http://user:secret@192.168.10.1", "http://192.168.10.1/path", "http://8.8.8.8"} { if _, err := NewReader(base, "secret"); !errors.Is(err, ErrRead) { t.Fatal("untrusted URL accepted") } } }