mirror of
https://github.com/zhoukk/dahua_api.git
synced 2025-12-24 13:29:27 +08:00
364 lines
8.1 KiB
Go
364 lines
8.1 KiB
Go
package dahua_api
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type wwwAuthenticate struct {
|
|
Algorithm string // unquoted
|
|
Domain string // quoted
|
|
Nonce string // quoted
|
|
Opaque string // quoted
|
|
Qop string // quoted
|
|
Realm string // quoted
|
|
Stale bool // unquoted
|
|
Charset string // quoted
|
|
Userhash bool // quoted
|
|
}
|
|
|
|
func newWwwAuthenticate(s string) *wwwAuthenticate {
|
|
|
|
wa := wwwAuthenticate{}
|
|
|
|
algorithmRegex := regexp.MustCompile(`algorithm="([^ ,]+)"`)
|
|
algorithmMatch := algorithmRegex.FindStringSubmatch(s)
|
|
if algorithmMatch != nil {
|
|
wa.Algorithm = algorithmMatch[1]
|
|
}
|
|
|
|
domainRegex := regexp.MustCompile(`domain="(.+?)"`)
|
|
domainMatch := domainRegex.FindStringSubmatch(s)
|
|
if domainMatch != nil {
|
|
wa.Domain = domainMatch[1]
|
|
}
|
|
|
|
nonceRegex := regexp.MustCompile(`nonce="(.+?)"`)
|
|
nonceMatch := nonceRegex.FindStringSubmatch(s)
|
|
if nonceMatch != nil {
|
|
wa.Nonce = nonceMatch[1]
|
|
}
|
|
|
|
opaqueRegex := regexp.MustCompile(`opaque="(.+?)"`)
|
|
opaqueMatch := opaqueRegex.FindStringSubmatch(s)
|
|
if opaqueMatch != nil {
|
|
wa.Opaque = opaqueMatch[1]
|
|
}
|
|
|
|
qopRegex := regexp.MustCompile(`qop="(.+?)"`)
|
|
qopMatch := qopRegex.FindStringSubmatch(s)
|
|
if qopMatch != nil {
|
|
wa.Qop = qopMatch[1]
|
|
}
|
|
|
|
realmRegex := regexp.MustCompile(`realm="(.+?)"`)
|
|
realmMatch := realmRegex.FindStringSubmatch(s)
|
|
if realmMatch != nil {
|
|
wa.Realm = realmMatch[1]
|
|
}
|
|
|
|
staleRegex := regexp.MustCompile(`stale=([^ ,])"`)
|
|
staleMatch := staleRegex.FindStringSubmatch(s)
|
|
if staleMatch != nil {
|
|
wa.Stale = (strings.ToLower(staleMatch[1]) == "true")
|
|
}
|
|
|
|
charsetRegex := regexp.MustCompile(`charset="(.+?)"`)
|
|
charsetMatch := charsetRegex.FindStringSubmatch(s)
|
|
if charsetMatch != nil {
|
|
wa.Charset = charsetMatch[1]
|
|
}
|
|
|
|
userhashRegex := regexp.MustCompile(`userhash=([^ ,])"`)
|
|
userhashMatch := userhashRegex.FindStringSubmatch(s)
|
|
if userhashMatch != nil {
|
|
wa.Userhash = (strings.ToLower(userhashMatch[1]) == "true")
|
|
}
|
|
|
|
return &wa
|
|
}
|
|
|
|
type authorization struct {
|
|
Algorithm string // unquoted
|
|
Cnonce string // quoted
|
|
Nc int // unquoted
|
|
Nonce string // quoted
|
|
Opaque string // quoted
|
|
Qop string // unquoted
|
|
Realm string // quoted
|
|
Response string // quoted
|
|
URI string // quoted
|
|
Userhash bool // quoted
|
|
Username string // quoted
|
|
}
|
|
|
|
func (wa *wwwAuthenticate) authorize(t *digestAuthTransport, req *http.Request, body string) *authorization {
|
|
|
|
a := &authorization{
|
|
Algorithm: wa.Algorithm,
|
|
Cnonce: "",
|
|
Nc: 0,
|
|
Nonce: wa.Nonce,
|
|
Opaque: wa.Opaque,
|
|
Qop: "",
|
|
Realm: wa.Realm,
|
|
Response: "",
|
|
URI: "",
|
|
Userhash: wa.Userhash,
|
|
Username: t.user,
|
|
}
|
|
|
|
if a.Userhash {
|
|
a.Username = a.hash(fmt.Sprintf("%s:%s", a.Username, a.Realm))
|
|
}
|
|
|
|
a.Nc++
|
|
|
|
a.Cnonce = a.hash(fmt.Sprintf("%d:%s:k", time.Now().UnixNano(), t.user))
|
|
a.URI = req.URL.RequestURI()
|
|
a.Response = a.computeResponse(wa, t, req, body)
|
|
|
|
return a
|
|
}
|
|
|
|
func (a *authorization) computeResponse(wa *wwwAuthenticate, t *digestAuthTransport, req *http.Request, body string) (s string) {
|
|
|
|
kdSecret := a.hash(a.computeA1(t))
|
|
kdData := fmt.Sprintf("%s:%08x:%s:%s:%s", a.Nonce, a.Nc, a.Cnonce, a.Qop, a.hash(a.computeA2(wa, t, req, body)))
|
|
|
|
return a.hash(fmt.Sprintf("%s:%s", kdSecret, kdData))
|
|
}
|
|
|
|
func (a *authorization) computeA1(t *digestAuthTransport) string {
|
|
|
|
algorithm := strings.ToUpper(a.Algorithm)
|
|
|
|
if algorithm == "" || algorithm == "MD5" || algorithm == "SHA-256" {
|
|
return fmt.Sprintf("%s:%s:%s", a.Username, a.Realm, t.pass)
|
|
}
|
|
|
|
if algorithm == "SHA-256" || algorithm == "SHA-256-SESS" {
|
|
upHash := a.hash(fmt.Sprintf("%s:%s:%s", a.Username, a.Realm, t.pass))
|
|
return fmt.Sprintf("%s:%s:%s", upHash, a.Nonce, a.Cnonce)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (a *authorization) computeA2(wa *wwwAuthenticate, t *digestAuthTransport, req *http.Request, body string) string {
|
|
|
|
if strings.Contains(wa.Qop, "auth-int") {
|
|
a.Qop = "auth-int"
|
|
return fmt.Sprintf("%s:%s:%s", req.Method, a.URI, a.hash(body))
|
|
}
|
|
|
|
if wa.Qop == "auth" || wa.Qop == "" {
|
|
a.Qop = "auth"
|
|
return fmt.Sprintf("%s:%s", req.Method, a.URI)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (a *authorization) hash(str string) string {
|
|
var h hash.Hash
|
|
algorithm := strings.ToUpper(a.Algorithm)
|
|
|
|
if algorithm == "" || algorithm == "MD5" || algorithm == "MD5-SESS" {
|
|
h = md5.New()
|
|
} else if algorithm == "SHA-256" || algorithm == "SHA-256-SESS" {
|
|
h = sha256.New()
|
|
} else {
|
|
return ""
|
|
}
|
|
|
|
io.WriteString(h, str)
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func (a *authorization) string() string {
|
|
var buf bytes.Buffer
|
|
|
|
buf.WriteString("Digest ")
|
|
|
|
if a.Username != "" {
|
|
buf.WriteString(fmt.Sprintf("username=\"%s\", ", a.Username))
|
|
}
|
|
|
|
if a.Realm != "" {
|
|
buf.WriteString(fmt.Sprintf("realm=\"%s\", ", a.Realm))
|
|
}
|
|
|
|
if a.Nonce != "" {
|
|
buf.WriteString(fmt.Sprintf("nonce=\"%s\", ", a.Nonce))
|
|
}
|
|
|
|
if a.URI != "" {
|
|
buf.WriteString(fmt.Sprintf("uri=\"%s\", ", a.URI))
|
|
}
|
|
|
|
if a.Response != "" {
|
|
buf.WriteString(fmt.Sprintf("response=\"%s\", ", a.Response))
|
|
}
|
|
|
|
if a.Algorithm != "" {
|
|
buf.WriteString(fmt.Sprintf("algorithm=%s, ", a.Algorithm))
|
|
}
|
|
|
|
if a.Cnonce != "" {
|
|
buf.WriteString(fmt.Sprintf("cnonce=\"%s\", ", a.Cnonce))
|
|
}
|
|
|
|
if a.Opaque != "" {
|
|
buf.WriteString(fmt.Sprintf("opaque=\"%s\", ", a.Opaque))
|
|
}
|
|
|
|
if a.Qop != "" {
|
|
buf.WriteString(fmt.Sprintf("qop=%s, ", a.Qop))
|
|
}
|
|
|
|
if a.Nc != 0 {
|
|
buf.WriteString(fmt.Sprintf("nc=%08x, ", a.Nc))
|
|
}
|
|
|
|
if a.Userhash {
|
|
buf.WriteString("userhash=true, ")
|
|
}
|
|
|
|
return strings.TrimSuffix(buf.String(), ", ")
|
|
}
|
|
|
|
type digestAuthTransport struct {
|
|
user string
|
|
pass string
|
|
transport http.RoundTripper
|
|
auth *wwwAuthenticate
|
|
}
|
|
|
|
func newTransport(user, pass string) *digestAuthTransport {
|
|
return &digestAuthTransport{
|
|
user,
|
|
pass,
|
|
http.DefaultTransport,
|
|
nil,
|
|
}
|
|
}
|
|
|
|
func (t *digestAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
rtreq := new(http.Request)
|
|
rtreq.Header = req.Header
|
|
rtreq.Method = req.Method
|
|
rtreq.URL = req.URL
|
|
|
|
reqbody := ""
|
|
if req.Body != nil {
|
|
buf, _ := ioutil.ReadAll(req.Body)
|
|
req.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
|
|
rtreq.Body = ioutil.NopCloser(bytes.NewBuffer(buf))
|
|
reqbody = string(buf)
|
|
}
|
|
|
|
if t.auth != nil {
|
|
auth := t.auth.authorize(t, req, reqbody)
|
|
req.Header.Set("Authorization", auth.string())
|
|
}
|
|
|
|
resp, err := t.transport.RoundTrip(req)
|
|
if err != nil || resp.StatusCode != 401 {
|
|
return resp, err
|
|
}
|
|
|
|
wwwauth := resp.Header.Get("WWW-Authenticate")
|
|
t.auth = newWwwAuthenticate(wwwauth)
|
|
auth := t.auth.authorize(t, req, reqbody)
|
|
|
|
resp.Body.Close()
|
|
rtreq.Header.Set("Authorization", auth.string())
|
|
|
|
return t.transport.RoundTrip(rtreq)
|
|
}
|
|
|
|
type DahuaApiClient struct {
|
|
proto string
|
|
host string
|
|
net *http.Client
|
|
}
|
|
|
|
func NewClient(host, user, pass string) *DahuaApiClient {
|
|
return &DahuaApiClient{"http", host, &http.Client{
|
|
Transport: newTransport(user, pass),
|
|
}}
|
|
}
|
|
|
|
func (c *DahuaApiClient) do(req *http.Request, ret map[string]string) error {
|
|
resp, err := c.net.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("http status:%d", resp.StatusCode)
|
|
}
|
|
|
|
br := bufio.NewReader(resp.Body)
|
|
for {
|
|
line, _, err := br.ReadLine()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
str := string(line)
|
|
arr := strings.Split(str, "=")
|
|
if len(arr) != 2 {
|
|
ret["status"] = str
|
|
} else {
|
|
ret[arr[0]] = arr[1]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *DahuaApiClient) CGI(cgi, action string, arg url.Values, ret map[string]string) error {
|
|
var buf strings.Builder
|
|
keys := make([]string, 0, len(arg))
|
|
for k := range arg {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
vs := arg[k]
|
|
keyEscaped := url.QueryEscape(k)
|
|
for _, v := range vs {
|
|
if buf.Len() > 0 {
|
|
buf.WriteByte('&')
|
|
}
|
|
buf.WriteString(keyEscaped)
|
|
buf.WriteByte('=')
|
|
buf.WriteString(url.PathEscape(v))
|
|
}
|
|
}
|
|
|
|
url := &url.URL{Scheme: c.proto, Host: c.host, Path: "/cgi-bin/" + cgi, RawQuery: "action=" + action + buf.String()}
|
|
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.do(req, ret)
|
|
}
|