Files
2026-10-11 14:59:51 +07:00

121 lines
4.4 KiB
Go

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(`<tr id="cbi-table-%s"><td><div id="cbi-table-%s-hostname"><p>PRIVATE NAME<br><span>%s</span></p></div></td><td><div id="cbi-table-%s-ipmac"><p>192.168.10.9<br>%s</p><p>192.168.10.9<br>%s</p></div></td><td><div id="cbi-table-%s-signal"><p>%s</p><p>%s</p></div></td></tr>`, id, id, band, id, mac, mac, id, signal, signal)
}
func clientTable(rows string) string {
return `<table><thead><tr><th>IP / MAC Address</th></tr></thead><tbody>` + rows + `</tbody></table>`
}
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{`<form>Login</form>`, 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, `<form><input type="hidden" name="salt" value="salt"><input type="hidden" name="_csrf" value="csrf"></form>`)
})
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")
}
}
}