generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user