From 28b8c3590df994be4054b12e78cdebb1a24a4d78 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Mon, 24 Nov 2025 09:50:58 +0800 Subject: [PATCH] fix: Fix the issue of abnormal WebDAV connection --- .../client/helper/webdav/webdav.go | 2 +- core/go.mod | 2 - core/go.sum | 7 - .../client/helper/webdav/auth.go | 306 ++++++++++++++++++ .../client/helper/webdav/auth_basic.go | 36 +++ .../client/helper/webdav/auth_digest.go | 173 ++++++++++ .../client/helper/webdav/auth_passport.go | 160 +++++++++ .../client/helper/webdav/errors.go | 35 ++ .../client/helper/webdav/file.go | 61 ++++ .../client/helper/webdav/reques.go | 95 ++++++ .../client/helper/webdav/utils.go | 95 ++++++ .../client/helper/webdav/webdav.go | 261 +++++++++++++++ core/utils/cloud_storage/client/webdav.go | 9 +- 13 files changed, 1227 insertions(+), 15 deletions(-) create mode 100644 core/utils/cloud_storage/client/helper/webdav/auth.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/auth_basic.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/auth_digest.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/auth_passport.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/errors.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/file.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/reques.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/utils.go create mode 100644 core/utils/cloud_storage/client/helper/webdav/webdav.go diff --git a/agent/utils/cloud_storage/client/helper/webdav/webdav.go b/agent/utils/cloud_storage/client/helper/webdav/webdav.go index 3200959ce..b26b30856 100644 --- a/agent/utils/cloud_storage/client/helper/webdav/webdav.go +++ b/agent/utils/cloud_storage/client/helper/webdav/webdav.go @@ -197,7 +197,7 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { return fmt.Errorf("mkdir %s failed, err: %v", item, err) } defer rs.Body.Close() - if rs.StatusCode != 201 || rs.StatusCode == 200 { + if rs.StatusCode != 201 && rs.StatusCode != 200 { return fmt.Errorf("mkdir %s failed, code: %d, err: %v", item, rs.StatusCode, rs.Status) } } diff --git a/core/go.mod b/core/go.mod index d592538e6..adb2f4eb5 100644 --- a/core/go.mod +++ b/core/go.mod @@ -36,7 +36,6 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/studio-b12/gowebdav v0.9.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.3 @@ -100,7 +99,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect diff --git a/core/go.sum b/core/go.sum index 5fc519d1c..714a02adc 100644 --- a/core/go.sum +++ b/core/go.sum @@ -315,10 +315,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -470,8 +466,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU= -github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= @@ -843,7 +837,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/utils/cloud_storage/client/helper/webdav/auth.go b/core/utils/cloud_storage/client/helper/webdav/auth.go new file mode 100644 index 000000000..b96ce0450 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/auth.go @@ -0,0 +1,306 @@ +package webdav + +import ( + "bytes" + "errors" + "io" + "net/http" + "strings" + "sync" +) + +type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) + +type Authorizer interface { + NewAuthenticator(body io.Reader) (Authenticator, io.Reader) + AddAuthenticator(key string, fn AuthFactory) +} + +type Authenticator interface { + Authorize(c *http.Client, rq *http.Request, path string) error + Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) + Clone() Authenticator + io.Closer +} + +type authfactory struct { + key string + create AuthFactory +} + +type authorizer struct { + factories []authfactory + defAuthMux sync.Mutex + defAuth Authenticator +} + +type preemptiveAuthorizer struct { + auth Authenticator +} + +type authShim struct { + factory AuthFactory + body io.Reader + auth Authenticator +} + +type negoAuth struct { + auths []Authenticator + setDefaultAuthenticator func(auth Authenticator) +} + +type nullAuth struct{} + +type noAuth struct{} + +func NewAutoAuth(login string, secret string) Authorizer { + fmap := make([]authfactory, 0) + az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}} + + az.AddAuthenticator("basic", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return &BasicAuth{user: login, pw: secret}, nil + }) + + az.AddAuthenticator("digest", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return NewDigestAuth(login, secret, rs) + }) + + az.AddAuthenticator("passport1.4", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return NewPassportAuth(c, login, secret, rs.Request.URL.String(), &rs.Header) + }) + + return az +} + +func NewEmptyAuth() Authorizer { + fmap := make([]authfactory, 0) + az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}} + return az +} + +func NewPreemptiveAuth(auth Authenticator) Authorizer { + return &preemptiveAuthorizer{auth: auth} +} + +func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { + var retryBuf io.Reader = body + if body != nil { + if _, ok := retryBuf.(io.Seeker); ok { + body = io.NopCloser(body) + } else { + buff := &bytes.Buffer{} + retryBuf = buff + body = io.TeeReader(body, buff) + } + } + a.defAuthMux.Lock() + defAuth := a.defAuth.Clone() + a.defAuthMux.Unlock() + + return &authShim{factory: a.factory, body: retryBuf, auth: defAuth}, body +} + +func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) { + key = strings.ToLower(key) + for _, f := range a.factories { + if f.key == key { + panic("Authenticator exists: " + key) + } + } + a.factories = append(a.factories, authfactory{key, fn}) +} + +func (a *authorizer) factory(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + headers := rs.Header.Values("Www-Authenticate") + if len(headers) > 0 { + auths := make([]Authenticator, 0) + for _, f := range a.factories { + for _, header := range headers { + headerLower := strings.ToLower(header) + if strings.Contains(headerLower, f.key) { + rs.Header.Set("Www-Authenticate", header) + if auth, err = f.create(c, rs, path); err == nil { + auths = append(auths, auth) + break + } + } + } + } + + switch len(auths) { + case 0: + return nil, NewPathError("NoAuthenticator", path, rs.StatusCode) + case 1: + auth = auths[0] + default: + auth = &negoAuth{auths: auths, setDefaultAuthenticator: a.setDefaultAuthenticator} + } + } else { + auth = &noAuth{} + } + + a.setDefaultAuthenticator(auth) + + return auth, nil +} + +func (a *authorizer) setDefaultAuthenticator(auth Authenticator) { + a.defAuthMux.Lock() + a.defAuth.Close() + a.defAuth = auth + a.defAuthMux.Unlock() +} + +func (s *authShim) Authorize(c *http.Client, rq *http.Request, path string) error { + if err := s.auth.Authorize(c, rq, path); err != nil { + return err + } + body := s.body + rq.GetBody = func() (io.ReadCloser, error) { + if body != nil { + if sk, ok := body.(io.Seeker); ok { + if _, err := sk.Seek(0, io.SeekStart); err != nil { + return nil, err + } + } + return io.NopCloser(body), nil + } + return nil, nil + } + return nil +} + +func (s *authShim) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + redo, err = s.auth.Verify(c, rs, path) + if err != nil && errors.Is(err, ErrAuthChanged) { + if auth, aerr := s.factory(c, rs, path); aerr == nil { + s.auth.Close() + s.auth = auth + return true, nil + } else { + return false, aerr + } + } + return +} + +func (s *authShim) Close() error { + s.auth.Close() + s.auth, s.factory = nil, nil + if s.body != nil { + if closer, ok := s.body.(io.Closer); ok { + return closer.Close() + } + } + return nil +} + +func (s *authShim) Clone() Authenticator { + return &noAuth{} +} + +func (s *authShim) String() string { + return "AuthShim" +} + +func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + if len(n.auths) == 0 { + return NewPathError("NoAuthenticator", path, 400) + } + return n.auths[0].Authorize(c, rq, path) +} + +func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if len(n.auths) == 0 { + return false, NewPathError("NoAuthenticator", path, 400) + } + redo, err = n.auths[0].Verify(c, rs, path) + if err != nil { + if len(n.auths) > 1 { + n.auths[0].Close() + n.auths = n.auths[1:] + return true, nil + } + } else if redo { + return + } else { + auth := n.auths[0] + n.auths = n.auths[1:] + n.setDefaultAuthenticator(auth) + return + } + + return false, NewPathError("NoAuthenticator", path, rs.StatusCode) +} + +func (n *negoAuth) Close() error { + for _, a := range n.auths { + a.Close() + } + n.setDefaultAuthenticator = nil + return nil +} + +func (n *negoAuth) Clone() Authenticator { + auths := make([]Authenticator, len(n.auths)) + for i, e := range n.auths { + auths[i] = e.Clone() + } + return &negoAuth{auths: auths, setDefaultAuthenticator: n.setDefaultAuthenticator} +} + +func (n *negoAuth) String() string { + return "NegoAuth" +} + +func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + return nil +} + +func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if "" != rs.Header.Get("Www-Authenticate") { + err = ErrAuthChanged + } + return +} + +func (n *noAuth) Close() error { + return nil +} + +func (n *noAuth) Clone() Authenticator { + return n +} + +func (n *noAuth) String() string { + return "NoAuth" +} + +func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + rq.Header.Set(XInhibitRedirect, "1") + return nil +} + +func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + return true, ErrAuthChanged +} + +func (n *nullAuth) Close() error { + return nil +} + +func (n *nullAuth) Clone() Authenticator { + return n +} + +func (n *nullAuth) String() string { + return "NullAuth" +} + +func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { + return b.auth.Clone(), body +} + +func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) { + panic("You're funny! A preemptive authorizer may only have a single authentication method") +} diff --git a/core/utils/cloud_storage/client/helper/webdav/auth_basic.go b/core/utils/cloud_storage/client/helper/webdav/auth_basic.go new file mode 100644 index 000000000..a73738537 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/auth_basic.go @@ -0,0 +1,36 @@ +package webdav + +import ( + "fmt" + "net/http" +) + +type BasicAuth struct { + user string + pw string +} + +func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + rq.SetBasicAuth(b.user, b.pw) + return nil +} + +func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if rs.StatusCode == 401 { + err = NewPathError("Authorize", path, rs.StatusCode) + } + return +} + +func (b *BasicAuth) Close() error { + return nil +} + +func (b *BasicAuth) Clone() Authenticator { + // no copy due to read only access + return b +} + +func (b *BasicAuth) String() string { + return fmt.Sprintf("BasicAuth login: %s", b.user) +} diff --git a/core/utils/cloud_storage/client/helper/webdav/auth_digest.go b/core/utils/cloud_storage/client/helper/webdav/auth_digest.go new file mode 100644 index 000000000..0e7026edc --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/auth_digest.go @@ -0,0 +1,173 @@ +package webdav + +import ( + "crypto/md5" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "maps" + "net/http" + "strings" +) + +type DigestAuth struct { + user string + pw string + digestParts map[string]string +} + +func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) { + return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil +} + +func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + d.digestParts["uri"] = path + d.digestParts["method"] = rq.Method + d.digestParts["username"] = d.user + d.digestParts["password"] = d.pw + rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts)) + return nil +} + +func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if rs.StatusCode == 401 { + if isStaled(rs) { + redo = true + err = ErrAuthChanged + } else { + err = NewPathError("Authorize", path, rs.StatusCode) + } + } + return +} + +func (d *DigestAuth) Close() error { + return nil +} + +func (d *DigestAuth) Clone() Authenticator { + var parts map[string]string + if parts = maps.Clone(parts); parts == nil { + parts = make(map[string]string) + } + return &DigestAuth{user: d.user, pw: d.pw, digestParts: parts} +} + +func (d *DigestAuth) String() string { + return fmt.Sprintf("DigestAuth login: %s", d.user) +} + +func digestParts(resp *http.Response) map[string]string { + result := map[string]string{} + if len(resp.Header["Www-Authenticate"]) > 0 { + wantedHeaders := []string{"nonce", "realm", "qop", "opaque", "algorithm", "entityBody"} + responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",") + for _, r := range responseHeaders { + for _, w := range wantedHeaders { + if strings.Contains(r, w) { + result[w] = strings.Trim( + strings.SplitN(r, `=`, 2)[1], + `"`, + ) + } + } + } + } + return result +} + +func getMD5(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func getCnonce() string { + b := make([]byte, 8) + io.ReadFull(rand.Reader, b) + return fmt.Sprintf("%x", b)[:16] +} + +func getDigestAuthorization(digestParts map[string]string) string { + d := digestParts + + var ( + ha1 string + ha2 string + nonceCount = 00000001 + cnonce = getCnonce() + response string + ) + + switch d["algorithm"] { + case "MD5", "": + ha1 = getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"]) + case "MD5-sess": + ha1 = getMD5( + fmt.Sprintf("%s:%v:%s", + getMD5(d["username"]+":"+d["realm"]+":"+d["password"]), + nonceCount, + cnonce, + ), + ) + } + + switch d["qop"] { + case "auth", "": + ha2 = getMD5(d["method"] + ":" + d["uri"]) + case "auth-int": + if d["entityBody"] != "" { + ha2 = getMD5(d["method"] + ":" + d["uri"] + ":" + getMD5(d["entityBody"])) + } + } + + switch d["qop"] { + case "": + response = getMD5( + fmt.Sprintf("%s:%s:%s", + ha1, + d["nonce"], + ha2, + ), + ) + case "auth", "auth-int": + response = getMD5( + fmt.Sprintf("%s:%s:%v:%s:%s:%s", + ha1, + d["nonce"], + nonceCount, + cnonce, + d["qop"], + ha2, + ), + ) + } + + authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", nc=%v, cnonce="%s", response="%s"`, + d["username"], d["realm"], d["nonce"], d["uri"], nonceCount, cnonce, response) + + if d["qop"] != "" { + authorization += fmt.Sprintf(`, qop=%s`, d["qop"]) + } + + if d["opaque"] != "" { + authorization += fmt.Sprintf(`, opaque="%s"`, d["opaque"]) + } + + return authorization +} + +func isStaled(rs *http.Response) bool { + header := rs.Header.Get("Www-Authenticate") + if len(header) > 0 { + directives := strings.Split(header, ",") + for i := range directives { + name, value, _ := strings.Cut(strings.Trim(directives[i], " "), "=") + if strings.EqualFold(name, "stale") { + return strings.EqualFold(value, "true") + } + } + } + return false +} diff --git a/core/utils/cloud_storage/client/helper/webdav/auth_passport.go b/core/utils/cloud_storage/client/helper/webdav/auth_passport.go new file mode 100644 index 000000000..fbbe9cc09 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/auth_passport.go @@ -0,0 +1,160 @@ +package webdav + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +type PassportAuth struct { + user string + pw string + cookies []http.Cookie + inhibitRedirect bool +} + +func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) { + p := &PassportAuth{ + user: user, + pw: pw, + inhibitRedirect: true, + } + err := p.genCookies(c, partnerURL, header) + return p, err +} + +func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + if p.inhibitRedirect { + rq.Header.Set(XInhibitRedirect, "1") + } else { + p.inhibitRedirect = true + } + for _, cookie := range p.cookies { + rq.AddCookie(&cookie) + } + return nil +} + +func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + switch rs.StatusCode { + case 301, 302, 307, 308: + redo = true + if rs.Header.Get("Www-Authenticate") != "" { + err = p.genCookies(c, rs.Request.URL.String(), &rs.Header) + } else { + p.inhibitRedirect = false + } + case 401: + err = NewPathError("Authorize", path, rs.StatusCode) + } + return +} + +func (p *PassportAuth) Close() error { + return nil +} + +func (p *PassportAuth) Clone() Authenticator { + clonedCookies := make([]http.Cookie, len(p.cookies)) + copy(clonedCookies, p.cookies) + + return &PassportAuth{ + user: p.user, + pw: p.pw, + cookies: clonedCookies, + inhibitRedirect: true, + } +} + +func (p *PassportAuth) String() string { + return fmt.Sprintf("PassportAuth login: %s", p.user) +} + +func (p *PassportAuth) genCookies(c *http.Client, partnerUrl string, header *http.Header) error { + baseAuthenticationServer := header.Get("Location") + baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer) + if err != nil { + return err + } + + authenticationServerUrl := url.URL{ + Scheme: baseAuthenticationServerURL.Scheme, + Host: baseAuthenticationServerURL.Host, + Path: "/login2.srf", + } + + partnerServerChallenge := strings.Split(header.Get("Www-Authenticate"), " ")[1] + + req := http.Request{ + Method: "GET", + URL: &authenticationServerUrl, + Header: http.Header{ + "Authorization": []string{"Passport1.4 sign-in=" + url.QueryEscape(p.user) + ",pwd=" + url.QueryEscape(p.pw) + ",OrgVerb=GET,OrgUrl=" + partnerUrl + "," + partnerServerChallenge}, + }, + } + + rs, err := c.Do(&req) + if err != nil { + return err + } + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + if rs.StatusCode != 200 { + return NewPathError("Authorize", "/", rs.StatusCode) + } + + tokenResponseHeader := rs.Header.Get("Authentication-Info") + if tokenResponseHeader == "" { + return NewPathError("Authorize", "/", 401) + } + tokenResponseHeaderList := strings.Split(tokenResponseHeader, ",") + token := "" + for _, tokenResponseHeader := range tokenResponseHeaderList { + if strings.HasPrefix(tokenResponseHeader, "from-PP='") { + token = tokenResponseHeader + break + } + } + if token == "" { + return NewPathError("Authorize", "/", 401) + } + + origUrl, err := url.Parse(partnerUrl) + if err != nil { + return err + } + req = http.Request{ + Method: "GET", + URL: origUrl, + Header: http.Header{ + "Authorization": []string{"Passport1.4 " + token}, + }, + } + + rs, err = c.Do(&req) + if err != nil { + return err + } + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + if rs.StatusCode != 200 && rs.StatusCode != 302 { + return NewPathError("Authorize", "/", rs.StatusCode) + } + + cookies := rs.Header.Values("Set-Cookie") + p.cookies = make([]http.Cookie, len(cookies)) + for i, cookie := range cookies { + cookieParts := strings.Split(cookie, ";") + cookieName := strings.Split(cookieParts[0], "=")[0] + cookieValue := strings.Split(cookieParts[0], "=")[1] + + p.cookies[i] = http.Cookie{ + Name: cookieName, + Value: cookieValue, + } + } + + return nil +} diff --git a/core/utils/cloud_storage/client/helper/webdav/errors.go b/core/utils/cloud_storage/client/helper/webdav/errors.go new file mode 100644 index 000000000..5448d31a0 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/errors.go @@ -0,0 +1,35 @@ +package webdav + +import ( + "errors" + "fmt" + "os" +) + +var ErrAuthChanged = errors.New("authentication failed, change algorithm") + +var ErrTooManyRedirects = errors.New("stopped after 10 redirects") + +type StatusError struct { + Status int +} + +func (se StatusError) Error() string { + return fmt.Sprintf("%d", se.Status) +} + +func NewPathError(op string, path string, statusCode int) error { + return &os.PathError{ + Op: op, + Path: path, + Err: StatusError{statusCode}, + } +} + +func NewPathErrorErr(op string, path string, err error) error { + return &os.PathError{ + Op: op, + Path: path, + Err: err, + } +} diff --git a/core/utils/cloud_storage/client/helper/webdav/file.go b/core/utils/cloud_storage/client/helper/webdav/file.go new file mode 100644 index 000000000..dc0157ab0 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/file.go @@ -0,0 +1,61 @@ +package webdav + +import ( + "fmt" + "os" + "time" +) + +type File struct { + path string + name string + contentType string + size int64 + modified time.Time + etag string + isdir bool +} + +func (f File) Name() string { + return f.name +} + +func (f File) ContentType() string { + return f.contentType +} + +func (f File) Size() int64 { + return f.size +} + +func (f File) Mode() os.FileMode { + if f.isdir { + return 0775 | os.ModeDir + } + + return 0664 +} + +func (f File) ModTime() time.Time { + return f.modified +} + +func (f File) ETag() string { + return f.etag +} + +func (f File) IsDir() bool { + return f.isdir +} + +func (f File) Sys() interface{} { + return nil +} + +func (f File) String() string { + if f.isdir { + return fmt.Sprintf("Dir : '%s' - '%s'", f.path, f.name) + } + + return fmt.Sprintf("File: '%s' SIZE: %d MODIFIED: %s ETAG: %s CTYPE: %s", f.path, f.size, f.modified.String(), f.etag, f.contentType) +} diff --git a/core/utils/cloud_storage/client/helper/webdav/reques.go b/core/utils/cloud_storage/client/helper/webdav/reques.go new file mode 100644 index 000000000..e8e2715c0 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/reques.go @@ -0,0 +1,95 @@ +package webdav + +import ( + "fmt" + "io" + "net/http" + "strings" +) + +func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) { + var redo bool + var r *http.Request + var uri = PathEscape(Join(c.root, path)) + auth, body := c.auth.NewAuthenticator(body) + defer auth.Close() + + for { + if r, err = http.NewRequest(method, uri, body); err != nil { + err = fmt.Errorf("handle request with uri: %s, method: %s failed, err: %v", uri, method, err) + return + } + + for k, vals := range c.headers { + for _, v := range vals { + r.Header.Add(k, v) + } + } + + if err = auth.Authorize(c.c, r, path); err != nil { + return + } + + if intercept != nil { + intercept(r) + } + + if rs, err = c.c.Do(r); err != nil { + err = fmt.Errorf("do request for resp with uri: %s, method: %s failed, err: %v", uri, method, err) + return + } + + if redo, err = auth.Verify(c.c, rs, path); err != nil { + rs.Body.Close() + return nil, err + } + if redo { + rs.Body.Close() + if body, err = r.GetBody(); err != nil { + return nil, err + } + continue + } + break + } + + return rs, err +} + +func (c *Client) propfind(path string, self bool, body string, resp interface{}, parse func(resp interface{}) error) error { + rs, err := c.req("PROPFIND", path, strings.NewReader(body), func(rq *http.Request) { + if self { + rq.Header.Add("Depth", "0") + } else { + rq.Header.Add("Depth", "1") + } + rq.Header.Add("Content-Type", "application/xml;charset=UTF-8") + rq.Header.Add("Accept", "application/xml,text/xml") + rq.Header.Add("Accept-Charset", "utf-8") + // TODO add support for 'gzip,deflate;q=0.8,q=0.7' + rq.Header.Add("Accept-Encoding", "") + }) + if err != nil { + return err + } + defer rs.Body.Close() + + if rs.StatusCode != 207 { + return NewPathError("PROPFIND", path, rs.StatusCode) + } + + return parseXML(rs.Body, resp, parse) +} + +func (c *Client) put(path string, stream io.Reader, contentLength int64) (status int, err error) { + rs, err := c.req("PUT", path, stream, func(r *http.Request) { + r.ContentLength = contentLength + }) + if err != nil { + return + } + defer rs.Body.Close() + + status = rs.StatusCode + return +} diff --git a/core/utils/cloud_storage/client/helper/webdav/utils.go b/core/utils/cloud_storage/client/helper/webdav/utils.go new file mode 100644 index 000000000..129018ff7 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/utils.go @@ -0,0 +1,95 @@ +package webdav + +import ( + "bytes" + "encoding/xml" + "io" + "net/url" + "path" + "strconv" + "strings" +) + +func PathEscape(path string) string { + s := strings.Split(path, "/") + for i, e := range s { + s[i] = url.PathEscape(e) + } + return strings.Join(s, "/") +} + +func FixSlash(s string) string { + if !strings.HasSuffix(s, "/") { + s += "/" + } + return s +} + +func SplitPathToHierarchy(fullPath string) []string { + cleanPath := path.Clean(fullPath) + parts := strings.Split(cleanPath, "/") + + var result []string + currentPath := "" + + for _, part := range parts { + if part == "" { + currentPath = "/" + result = append(result, currentPath) + continue + } + + if currentPath == "/" { + currentPath = path.Join(currentPath, part) + } else { + currentPath = path.Join(currentPath, part) + } + + result = append(result, currentPath) + } + + return result +} + +func FixSlashes(s string) string { + if !strings.HasPrefix(s, "/") { + s = "/" + s + } + + return FixSlash(s) +} + +func Join(path0 string, path1 string) string { + return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/") +} + +func String(r io.Reader) string { + buf := new(bytes.Buffer) + // TODO - make String return an error as well + _, _ = buf.ReadFrom(r) + return buf.String() +} + +func parseInt64(s *string) int64 { + if n, e := strconv.ParseInt(*s, 10, 64); e == nil { + return n + } + return 0 +} + +func parseXML(data io.Reader, resp interface{}, parse func(resp interface{}) error) error { + decoder := xml.NewDecoder(data) + for t, _ := decoder.Token(); t != nil; t, _ = decoder.Token() { + switch se := t.(type) { + case xml.StartElement: + if se.Name.Local == "response" { + if e := decoder.DecodeElement(resp, &se); e == nil { + if err := parse(resp); err != nil { + return err + } + } + } + } + } + return nil +} diff --git a/core/utils/cloud_storage/client/helper/webdav/webdav.go b/core/utils/cloud_storage/client/helper/webdav/webdav.go new file mode 100644 index 000000000..b26b30856 --- /dev/null +++ b/core/utils/cloud_storage/client/helper/webdav/webdav.go @@ -0,0 +1,261 @@ +package webdav + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" +const template = ` + + + + + +` + +type Client struct { + root string + headers http.Header + c *http.Client + auth Authorizer +} + +func NewClient(uri, user, pw string) *Client { + return NewAuthClient(uri, NewAutoAuth(user, pw)) +} + +func NewAuthClient(uri string, auth Authorizer) *Client { + c := &http.Client{ + CheckRedirect: func(rq *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return ErrTooManyRedirects + } + if via[0].Header.Get(XInhibitRedirect) != "" { + return http.ErrUseLastResponse + } + return nil + }, + } + return &Client{root: FixSlash(uri), headers: make(http.Header), c: c, auth: auth} +} + +func (c *Client) SetTransport(transport http.RoundTripper) { + c.c.Transport = transport +} + +func (c *Client) Connect() error { + rs, err := c.req("OPTIONS", "/", nil, func(rq *http.Request) { rq.Header.Add("Depth", "0") }) + if err != nil { + return err + } + defer rs.Body.Close() + + if rs.StatusCode != 200 && rs.StatusCode != 204 { + return fmt.Errorf("check conn failed, code: %d, err: %v", rs.StatusCode, rs.Status) + } + + return nil +} + +type props struct { + Status string `xml:"DAV: status"` + Name string `xml:"DAV: prop>displayname,omitempty"` + Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"` + Size string `xml:"DAV: prop>getcontentlength,omitempty"` +} + +type response struct { + Href string `xml:"DAV: href"` + Props []props `xml:"DAV: propstat"` +} + +func getProps(r *response, status string) *props { + for _, prop := range r.Props { + if strings.Contains(prop.Status, status) { + return &prop + } + } + return nil +} + +func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { + path = FixSlashes(path) + files := make([]os.FileInfo, 0) + skipSelf := true + parse := func(resp interface{}) error { + r := resp.(*response) + + if skipSelf { + skipSelf = false + if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" { + r.Props = nil + return nil + } + return NewPathError("ReadDir", path, 405) + } + + if p := getProps(r, "200"); p != nil { + f := new(File) + if ps, err := url.PathUnescape(r.Href); err == nil { + f.name = filepath.Base(ps) + } else { + f.name = p.Name + } + f.path = path + f.name + if p.Type.Local == "collection" { + f.path += "/" + f.size = 0 + f.isdir = true + } else { + f.size = parseInt64(&p.Size) + f.isdir = false + } + + files = append(files, *f) + } + + r.Props = nil + return nil + } + + if err := c.propfind(path, false, template, &response{}, parse); err != nil { + if _, ok := err.(*os.PathError); !ok { + return files, fmt.Errorf("load files from %s failed, err: %v", path, err) + } + } + return files, nil +} + +func (c *Client) Stat(path string) (os.FileInfo, error) { + var f *File + parse := func(resp interface{}) error { + r := resp.(*response) + if p := getProps(r, "200"); p != nil && f == nil { + f = new(File) + f.name = p.Name + f.path = path + + if p.Type.Local == "collection" { + if !strings.HasSuffix(f.path, "/") { + f.path += "/" + } + f.size = 0 + f.isdir = true + } else { + f.size = parseInt64(&p.Size) + f.isdir = false + } + } + + r.Props = nil + return nil + } + + if err := c.propfind(path, true, template, &response{}, parse); err != nil { + if _, ok := err.(*os.PathError); !ok { + return f, fmt.Errorf("load file %s failed, path err: %v", path, err) + } + return f, fmt.Errorf("load file %s failed, err: %v", path, err) + } + return f, nil +} + +func (c *Client) RemoveAll(path string) error { + rs, err := c.req("DELETE", path, nil, nil) + if err != nil { + return fmt.Errorf("handle remove file %s failed, err: %s", path, err) + } + defer rs.Body.Close() + if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 { + return nil + } + return fmt.Errorf("handle remove file %s failed, code: %d, err: %s", path, rs.StatusCode, rs.Status) +} + +func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { + parentPath := filepath.Dir(path) + if parentPath == "." || parentPath == "/" { + return nil + } + + paths := SplitPathToHierarchy(parentPath) + for _, item := range paths { + itemFile, err := c.Stat(item) + if err == nil && itemFile.IsDir() { + continue + } + rs, err := c.req("MKCOL", item, nil, nil) + if err != nil { + return fmt.Errorf("mkdir %s failed, err: %v", item, err) + } + defer rs.Body.Close() + if rs.StatusCode != 201 && rs.StatusCode != 200 { + return fmt.Errorf("mkdir %s failed, code: %d, err: %v", item, rs.StatusCode, rs.Status) + } + } + return nil +} + +func (c *Client) ReadStream(path string) (io.ReadCloser, error) { + rs, err := c.req("GET", path, nil, nil) + if err != nil { + return nil, NewPathErrorErr("ReadStream", path, err) + } + + if rs.StatusCode == 200 { + return rs.Body, nil + } + + rs.Body.Close() + return nil, NewPathError("ReadStream", path, rs.StatusCode) +} + +func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) { + err = c.MkdirAll(path, 0755) + if err != nil { + return err + } + + contentLength := int64(0) + if seeker, ok := stream.(io.Seeker); ok { + contentLength, err = seeker.Seek(0, io.SeekEnd) + if err != nil { + return err + } + + _, err = seeker.Seek(0, io.SeekStart) + if err != nil { + return err + } + } else { + buffer := bytes.NewBuffer(make([]byte, 0, 1024*1024 /* 1MB */)) + + contentLength, err = io.Copy(buffer, stream) + if err != nil { + return err + } + + stream = buffer + } + + s, err := c.put(path, stream, contentLength) + if err != nil { + return err + } + + switch s { + case 200, 201, 204: + return nil + + default: + return NewPathError("WriteStream", path, s) + } +} diff --git a/core/utils/cloud_storage/client/webdav.go b/core/utils/cloud_storage/client/webdav.go index 89d4c36f2..c0c032a47 100644 --- a/core/utils/cloud_storage/client/webdav.go +++ b/core/utils/cloud_storage/client/webdav.go @@ -9,13 +9,12 @@ import ( "strings" "github.com/1Panel-dev/1Panel/core/constant" - - "github.com/studio-b12/gowebdav" + "github.com/1Panel-dev/1Panel/core/utils/cloud_storage/client/helper/webdav" ) type webDAVClient struct { Bucket string - client *gowebdav.Client + client *webdav.Client } func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) { @@ -29,7 +28,7 @@ func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) { if len(port) == 0 { url = address } - client := gowebdav.NewClient(url, username, password) + client := webdav.NewClient(url, username, password) tlsConfig := &tls.Config{} if strings.HasPrefix(address, "https") { tlsConfig.InsecureSkipVerify = true @@ -64,7 +63,7 @@ func (s webDAVClient) ListBuckets() ([]interface{}, error) { } func (s webDAVClient) Delete(pathItem string) (bool, error) { - if err := s.client.Remove(path.Join(s.Bucket, pathItem)); err != nil { + if err := s.client.RemoveAll(path.Join(s.Bucket, pathItem)); err != nil { return false, err } return true, nil