package cudy import ( "context" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "net/http/cookiejar" "net/netip" "net/url" "strings" "time" "golang.org/x/net/html" ) var ErrRead = errors.New("не удалось прочитать Wi-Fi Cudy") const maxBody = 2 << 20 type reader struct { base string password string http *http.Client authenticated bool } func NewReader(base, password string) (Reader, error) { u, err := url.Parse(base) if err != nil || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || (u.Scheme != "http" && u.Scheme != "https") { return nil, ErrRead } ip, err := netip.ParseAddr(u.Hostname()) if err != nil || (!ip.IsPrivate() && !ip.IsLoopback()) || password == "" { return nil, ErrRead } jar, _ := cookiejar.New(nil) client := &http.Client{Timeout: 5 * time.Second, Jar: jar, Transport: &http.Transport{Proxy: nil}, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) > 5 || req.URL.Host != u.Host || req.URL.Scheme != u.Scheme { return ErrRead } return nil }} return &reader{base: strings.TrimRight(base, "/"), password: password, http: client}, nil } func (r *reader) request(ctx context.Context, path string, form url.Values) ([]byte, int, error) { method := http.MethodGet var body io.Reader if form != nil { method = http.MethodPost body = strings.NewReader(form.Encode()) } req, err := http.NewRequestWithContext(ctx, method, r.base+path, body) if err != nil { return nil, 0, ErrRead } if form != nil { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } resp, err := r.http.Do(req) if err != nil { if ctx.Err() != nil { return nil, 0, ctx.Err() } return nil, 0, ErrRead } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, maxBody+1)) if err != nil || len(data) > maxBody { return nil, resp.StatusCode, ErrRead } return data, resp.StatusCode, nil } func (r *reader) login(ctx context.Context) error { data, code, err := r.request(ctx, "/cgi-bin/luci/", nil) if err != nil { return err } if code != http.StatusForbidden && code != http.StatusOK { return ErrRead } doc, err := html.Parse(strings.NewReader(string(data))) if err != nil { return ErrRead } form := url.Values{} walk(doc, func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "input" && attr(n, "type") == "hidden" { switch attr(n, "name") { case "_csrf", "salt", "token": form.Set(attr(n, "name"), attr(n, "value")) } } }) if form.Get("salt") == "" || form.Get("_csrf") == "" { return ErrRead } token, code, err := r.request(ctx, "/cgi-bin/luci/admin/get_token", url.Values{}) if err != nil { return err } if code != http.StatusOK || len(token) > 256 || len(token) == 0 { return ErrRead } hash := sha256.Sum256([]byte(r.password + form.Get("salt"))) hash = sha256.Sum256([]byte(hex.EncodeToString(hash[:]) + strings.TrimSpace(string(token)))) form.Set("luci_username", "admin") form.Set("luci_password", hex.EncodeToString(hash[:])) form.Set("token", strings.TrimSpace(string(token))) form.Set("luci_language", "en") data, code, err = r.request(ctx, "/cgi-bin/luci/", form) if err != nil { return err } if code != http.StatusOK || strings.Contains(string(data), "luci_password_login") { return ErrRead } r.authenticated = true return nil } func (r *reader) Read(ctx context.Context) ([]Client, error) { if !r.authenticated { if err := r.login(ctx); err != nil { return nil, err } } data, code, err := r.request(ctx, "/cgi-bin/luci/admin/network/devices/devlist?detail=1", nil) if err != nil { return nil, err } if code == http.StatusForbidden || code == http.StatusUnauthorized || strings.Contains(string(data), "luci_password_login") { r.authenticated = false if err := r.login(ctx); err != nil { return nil, err } data, code, err = r.request(ctx, "/cgi-bin/luci/admin/network/devices/devlist?detail=1", nil) if err != nil { return nil, err } } if code != http.StatusOK { return nil, ErrRead } return parseClients(data) } func attr(n *html.Node, key string) string { for _, a := range n.Attr { if a.Key == key { return a.Val } } return "" } func walk(n *html.Node, visit func(*html.Node)) { visit(n) for c := n.FirstChild; c != nil; c = c.NextSibling { walk(c, visit) } } func nodeText(n *html.Node) string { var out strings.Builder walk(n, func(c *html.Node) { if c.Type == html.TextNode { out.WriteString(c.Data) out.WriteByte(' ') } }) return strings.TrimSpace(out.String()) }