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

176 lines
4.5 KiB
Go

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())
}