mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-18 05:19:19 +08:00
fix: Fix the issue of abnormal WebDAV connection
This commit is contained in:
parent
7389aad122
commit
28b8c3590d
13 changed files with 1227 additions and 15 deletions
|
|
@ -197,7 +197,7 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
|
||||||
return fmt.Errorf("mkdir %s failed, err: %v", item, err)
|
return fmt.Errorf("mkdir %s failed, err: %v", item, err)
|
||||||
}
|
}
|
||||||
defer rs.Body.Close()
|
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)
|
return fmt.Errorf("mkdir %s failed, code: %d, err: %v", item, rs.StatusCode, rs.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ require (
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
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/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
|
|
@ -100,7 +99,6 @@ require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // 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/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
|
|
|
||||||
|
|
@ -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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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.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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
306
core/utils/cloud_storage/client/helper/webdav/auth.go
Normal file
306
core/utils/cloud_storage/client/helper/webdav/auth.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
36
core/utils/cloud_storage/client/helper/webdav/auth_basic.go
Normal file
36
core/utils/cloud_storage/client/helper/webdav/auth_basic.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
173
core/utils/cloud_storage/client/helper/webdav/auth_digest.go
Normal file
173
core/utils/cloud_storage/client/helper/webdav/auth_digest.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
160
core/utils/cloud_storage/client/helper/webdav/auth_passport.go
Normal file
160
core/utils/cloud_storage/client/helper/webdav/auth_passport.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
35
core/utils/cloud_storage/client/helper/webdav/errors.go
Normal file
35
core/utils/cloud_storage/client/helper/webdav/errors.go
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
61
core/utils/cloud_storage/client/helper/webdav/file.go
Normal file
61
core/utils/cloud_storage/client/helper/webdav/file.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
95
core/utils/cloud_storage/client/helper/webdav/reques.go
Normal file
95
core/utils/cloud_storage/client/helper/webdav/reques.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
95
core/utils/cloud_storage/client/helper/webdav/utils.go
Normal file
95
core/utils/cloud_storage/client/helper/webdav/utils.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
261
core/utils/cloud_storage/client/helper/webdav/webdav.go
Normal file
261
core/utils/cloud_storage/client/helper/webdav/webdav.go
Normal file
|
|
@ -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 = `<d:propfind xmlns:d='DAV:'>
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname/>
|
||||||
|
<d:resourcetype/>
|
||||||
|
<d:getcontentlength/>
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>`
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,13 +9,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/core/constant"
|
"github.com/1Panel-dev/1Panel/core/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/core/utils/cloud_storage/client/helper/webdav"
|
||||||
"github.com/studio-b12/gowebdav"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type webDAVClient struct {
|
type webDAVClient struct {
|
||||||
Bucket string
|
Bucket string
|
||||||
client *gowebdav.Client
|
client *webdav.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
|
func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
|
||||||
|
|
@ -29,7 +28,7 @@ func NewWebDAVClient(vars map[string]interface{}) (*webDAVClient, error) {
|
||||||
if len(port) == 0 {
|
if len(port) == 0 {
|
||||||
url = address
|
url = address
|
||||||
}
|
}
|
||||||
client := gowebdav.NewClient(url, username, password)
|
client := webdav.NewClient(url, username, password)
|
||||||
tlsConfig := &tls.Config{}
|
tlsConfig := &tls.Config{}
|
||||||
if strings.HasPrefix(address, "https") {
|
if strings.HasPrefix(address, "https") {
|
||||||
tlsConfig.InsecureSkipVerify = true
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
|
@ -64,7 +63,7 @@ func (s webDAVClient) ListBuckets() ([]interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s webDAVClient) Delete(pathItem string) (bool, 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 false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue