mirror of https://gitee.com/godoos/godoos.git
18 changed files with 2130 additions and 63 deletions
@ -0,0 +1,409 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"bytes" |
|||
"errors" |
|||
"io" |
|||
"net/http" |
|||
"strings" |
|||
"sync" |
|||
) |
|||
|
|||
// AuthFactory prototype function to create a new Authenticator
|
|||
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) |
|||
|
|||
// Authorizer our Authenticator factory which creates an
|
|||
// `Authenticator` per action/request.
|
|||
type Authorizer interface { |
|||
// Creates a new Authenticator Shim per request.
|
|||
// It may track request related states and perform payload buffering
|
|||
// for authentication round trips.
|
|||
// The underlying Authenticator will perform the real authentication.
|
|||
NewAuthenticator(body io.Reader) (Authenticator, io.Reader) |
|||
// Registers a new Authenticator factory to a key.
|
|||
AddAuthenticator(key string, fn AuthFactory) |
|||
} |
|||
|
|||
// An Authenticator implements a specific way to authorize requests.
|
|||
// Each request is bound to a separate Authenticator instance.
|
|||
//
|
|||
// The authentication flow itself is broken down into `Authorize`
|
|||
// and `Verify` steps. The former method runs before, and the latter
|
|||
// runs after the `Request` is submitted.
|
|||
// This makes it easy to encapsulate and control complex
|
|||
// authentication challenges.
|
|||
//
|
|||
// Some authentication flows causing authentication round trips,
|
|||
// which can be archived by returning the `redo` of the Verify
|
|||
// method. `True` restarts the authentication process for the
|
|||
// current action: A new `Request` is spawned, which must be
|
|||
// authorized, sent, and re-verified again, until the action
|
|||
// is successfully submitted.
|
|||
// The preferred way is to handle the authentication ping-pong
|
|||
// within `Verify`, and then `redo` with fresh credentials.
|
|||
//
|
|||
// The result of the `Verify` method can also trigger an
|
|||
// `Authenticator` change by returning the `ErrAuthChanged`
|
|||
// as an error. Depending on the `Authorizer` this may trigger
|
|||
// an `Authenticator` negotiation.
|
|||
//
|
|||
// Set the `XInhibitRedirect` header to '1' in the `Authorize`
|
|||
// method to get control over request redirection.
|
|||
// Attention! You must handle the incoming request yourself.
|
|||
//
|
|||
// To store a shared session state the `Clone` method **must**
|
|||
// return a new instance, initialized with the shared state.
|
|||
type Authenticator interface { |
|||
// Authorizes a request. Usually by adding some authorization headers.
|
|||
Authorize(c *http.Client, rq *http.Request, path string) error |
|||
// Verifies the response if the authorization was successful.
|
|||
// May trigger some round trips to pass the authentication.
|
|||
// May also trigger a new Authenticator negotiation by returning `ErrAuthChenged`
|
|||
Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) |
|||
// Creates a copy of the underlying Authenticator.
|
|||
Clone() Authenticator |
|||
io.Closer |
|||
} |
|||
|
|||
type authfactory struct { |
|||
key string |
|||
create AuthFactory |
|||
} |
|||
|
|||
// authorizer structure holds our Authenticator create functions
|
|||
type authorizer struct { |
|||
factories []authfactory |
|||
defAuthMux sync.Mutex |
|||
defAuth Authenticator |
|||
} |
|||
|
|||
// preemptiveAuthorizer structure holds the preemptive Authenticator
|
|||
type preemptiveAuthorizer struct { |
|||
auth Authenticator |
|||
} |
|||
|
|||
// authShim structure that wraps the real Authenticator
|
|||
type authShim struct { |
|||
factory AuthFactory |
|||
body io.Reader |
|||
auth Authenticator |
|||
} |
|||
|
|||
// negoAuth structure holds the authenticators that are going to be negotiated
|
|||
type negoAuth struct { |
|||
auths []Authenticator |
|||
setDefaultAuthenticator func(auth Authenticator) |
|||
} |
|||
|
|||
// nullAuth initializes the whole authentication flow
|
|||
type nullAuth struct{} |
|||
|
|||
// noAuth structure to perform no authentication at all
|
|||
type noAuth struct{} |
|||
|
|||
// NewAutoAuth creates an auto Authenticator factory.
|
|||
// It negotiates the default authentication method
|
|||
// based on the order of the registered Authenticators
|
|||
// and the remotely offered authentication methods.
|
|||
// First In, First Out.
|
|||
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 |
|||
} |
|||
|
|||
// NewEmptyAuth creates an empty Authenticator factory
|
|||
// The order of adding the Authenticator matters.
|
|||
// First In, First Out.
|
|||
// It offers the `NewAutoAuth` features.
|
|||
func NewEmptyAuth() Authorizer { |
|||
fmap := make([]authfactory, 0) |
|||
az := &authorizer{factories: fmap, defAuthMux: sync.Mutex{}, defAuth: &nullAuth{}} |
|||
return az |
|||
} |
|||
|
|||
// NewPreemptiveAuth creates a preemptive Authenticator
|
|||
// The preemptive authorizer uses the provided Authenticator
|
|||
// for every request regardless of any `Www-Authenticate` header.
|
|||
//
|
|||
// It may only have one authentication method,
|
|||
// so calling `AddAuthenticator` **will panic**!
|
|||
//
|
|||
// Look out!! This offers the skinniest and slickest implementation
|
|||
// without any synchronisation!!
|
|||
// Still applicable with `BasicAuth` within go routines.
|
|||
func NewPreemptiveAuth(auth Authenticator) Authorizer { |
|||
return &preemptiveAuthorizer{auth: auth} |
|||
} |
|||
|
|||
// NewAuthenticator creates an Authenticator (Shim) per request
|
|||
func (a *authorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { |
|||
var retryBuf io.Reader = body |
|||
if body != nil { |
|||
// If the authorization fails, we will need to restart reading
|
|||
// from the passed body stream.
|
|||
// When body is seekable, use seek to reset the streams
|
|||
// cursor to the start.
|
|||
// Otherwise, copy the stream into a buffer while uploading
|
|||
// and use the buffers content on retry.
|
|||
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 |
|||
} |
|||
|
|||
// AddAuthenticator appends the AuthFactory to our factories.
|
|||
// It converts the key to lower case and preserves the order.
|
|||
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}) |
|||
} |
|||
|
|||
// factory picks all valid Authenticators based on Www-Authenticate headers
|
|||
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 |
|||
} |
|||
|
|||
// setDefaultAuthenticator sets the default Authenticator
|
|||
func (a *authorizer) setDefaultAuthenticator(auth Authenticator) { |
|||
a.defAuthMux.Lock() |
|||
a.defAuth.Close() |
|||
a.defAuth = auth |
|||
a.defAuthMux.Unlock() |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
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 |
|||
} |
|||
|
|||
// Verify checks for authentication issues and may trigger a re-authentication.
|
|||
// Catches AlgoChangedErr to update the current Authenticator
|
|||
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 |
|||
} |
|||
|
|||
// Close closes all resources
|
|||
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 |
|||
} |
|||
|
|||
// It's not intend to Clone the shim
|
|||
// therefore it returns a noAuth instance
|
|||
func (s *authShim) Clone() Authenticator { |
|||
return &noAuth{} |
|||
} |
|||
|
|||
// String toString
|
|||
func (s *authShim) String() string { |
|||
return "AuthShim" |
|||
} |
|||
|
|||
// Authorize authorizes the current request with the top most Authorizer
|
|||
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) |
|||
} |
|||
|
|||
// Verify verifies the authentication and selects the next one based on the result
|
|||
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) |
|||
} |
|||
|
|||
// Close will close the underlying authenticators.
|
|||
func (n *negoAuth) Close() error { |
|||
for _, a := range n.auths { |
|||
a.Close() |
|||
} |
|||
n.setDefaultAuthenticator = nil |
|||
return nil |
|||
} |
|||
|
|||
// Clone clones the underlying authenticators.
|
|||
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" |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
|||
return nil |
|||
} |
|||
|
|||
// Verify checks for authentication issues and may trigger a re-authentication
|
|||
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 |
|||
} |
|||
|
|||
// Close closes all resources
|
|||
func (n *noAuth) Close() error { |
|||
return nil |
|||
} |
|||
|
|||
// Clone creates a copy of itself
|
|||
func (n *noAuth) Clone() Authenticator { |
|||
// no copy due to read only access
|
|||
return n |
|||
} |
|||
|
|||
// String toString
|
|||
func (n *noAuth) String() string { |
|||
return "NoAuth" |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
|||
rq.Header.Set(XInhibitRedirect, "1") |
|||
return nil |
|||
} |
|||
|
|||
// Verify checks for authentication issues and may trigger a re-authentication
|
|||
func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { |
|||
return true, ErrAuthChanged |
|||
} |
|||
|
|||
// Close closes all resources
|
|||
func (n *nullAuth) Close() error { |
|||
return nil |
|||
} |
|||
|
|||
// Clone creates a copy of itself
|
|||
func (n *nullAuth) Clone() Authenticator { |
|||
// no copy due to read only access
|
|||
return n |
|||
} |
|||
|
|||
// String toString
|
|||
func (n *nullAuth) String() string { |
|||
return "NullAuth" |
|||
} |
|||
|
|||
// NewAuthenticator creates an Authenticator (Shim) per request
|
|||
func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) { |
|||
return b.auth.Clone(), body |
|||
} |
|||
|
|||
// AddAuthenticator Will PANIC because it may only have a single authentication method
|
|||
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) { |
|||
panic("You're funny! A preemptive authorizer may only have a single authentication method") |
|||
} |
@ -0,0 +1,42 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/http" |
|||
) |
|||
|
|||
// BasicAuth structure holds our credentials
|
|||
type BasicAuth struct { |
|||
user string |
|||
pw string |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
|||
rq.SetBasicAuth(b.user, b.pw) |
|||
return nil |
|||
} |
|||
|
|||
// Verify verifies if the authentication
|
|||
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 |
|||
} |
|||
|
|||
// Close cleans up all resources
|
|||
func (b *BasicAuth) Close() error { |
|||
return nil |
|||
} |
|||
|
|||
// Clone creates a Copy of itself
|
|||
func (b *BasicAuth) Clone() Authenticator { |
|||
// no copy due to read only access
|
|||
return b |
|||
} |
|||
|
|||
// String toString
|
|||
func (b *BasicAuth) String() string { |
|||
return fmt.Sprintf("BasicAuth login: %s", b.user) |
|||
} |
@ -0,0 +1,438 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/xml" |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"net/url" |
|||
"os" |
|||
pathpkg "path" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" |
|||
|
|||
// Client defines our structure
|
|||
type Client struct { |
|||
root string |
|||
headers http.Header |
|||
interceptor func(method string, rq *http.Request) |
|||
c *http.Client |
|||
auth Authorizer |
|||
} |
|||
|
|||
// NewClient creates a new instance of client
|
|||
func NewClient(uri, user, pw string) *Client { |
|||
return NewAuthClient(uri, NewAutoAuth(user, pw)) |
|||
} |
|||
|
|||
// NewAuthClient creates a new client instance with a custom Authorizer
|
|||
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), interceptor: nil, c: c, auth: auth} |
|||
} |
|||
|
|||
// SetHeader lets us set arbitrary headers for a given client
|
|||
func (c *Client) SetHeader(key, value string) { |
|||
c.headers.Add(key, value) |
|||
} |
|||
|
|||
// SetInterceptor lets us set an arbitrary interceptor for a given client
|
|||
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) { |
|||
c.interceptor = interceptor |
|||
} |
|||
|
|||
// SetTimeout exposes the ability to set a time limit for requests
|
|||
func (c *Client) SetTimeout(timeout time.Duration) { |
|||
c.c.Timeout = timeout |
|||
} |
|||
|
|||
// SetTransport exposes the ability to define custom transports
|
|||
func (c *Client) SetTransport(transport http.RoundTripper) { |
|||
c.c.Transport = transport |
|||
} |
|||
|
|||
// SetJar exposes the ability to set a cookie jar to the client.
|
|||
func (c *Client) SetJar(jar http.CookieJar) { |
|||
c.c.Jar = jar |
|||
} |
|||
|
|||
// Connect connects to our dav server
|
|||
func (c *Client) Connect() error { |
|||
rs, err := c.options("/") |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
err = rs.Body.Close() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if rs.StatusCode != 200 { |
|||
return NewPathError("Connect", c.root, rs.StatusCode) |
|||
} |
|||
|
|||
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"` |
|||
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"` |
|||
ETag string `xml:"DAV: prop>getetag,omitempty"` |
|||
Modified string `xml:"DAV: prop>getlastmodified,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 |
|||
} |
|||
|
|||
// ReadDir reads the contents of a remote directory
|
|||
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 = pathpkg.Base(ps) |
|||
} else { |
|||
f.name = p.Name |
|||
} |
|||
f.path = path + f.name |
|||
f.modified = parseModified(&p.Modified) |
|||
f.etag = p.ETag |
|||
f.contentType = p.ContentType |
|||
|
|||
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 |
|||
} |
|||
|
|||
err := c.propfind(path, false, |
|||
`<d:propfind xmlns:d='DAV:'> |
|||
<d:prop> |
|||
<d:displayname/> |
|||
<d:resourcetype/> |
|||
<d:getcontentlength/> |
|||
<d:getcontenttype/> |
|||
<d:getetag/> |
|||
<d:getlastmodified/> |
|||
</d:prop> |
|||
</d:propfind>`, |
|||
&response{}, |
|||
parse) |
|||
|
|||
if err != nil { |
|||
if _, ok := err.(*os.PathError); !ok { |
|||
err = NewPathErrorErr("ReadDir", path, err) |
|||
} |
|||
} |
|||
return files, err |
|||
} |
|||
|
|||
// Stat returns the file stats for a specified path
|
|||
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 |
|||
f.etag = p.ETag |
|||
f.contentType = p.ContentType |
|||
|
|||
if p.Type.Local == "collection" { |
|||
if !strings.HasSuffix(f.path, "/") { |
|||
f.path += "/" |
|||
} |
|||
f.size = 0 |
|||
f.modified = parseModified(&p.Modified) |
|||
f.isdir = true |
|||
} else { |
|||
f.size = parseInt64(&p.Size) |
|||
f.modified = parseModified(&p.Modified) |
|||
f.isdir = false |
|||
} |
|||
} |
|||
|
|||
r.Props = nil |
|||
return nil |
|||
} |
|||
|
|||
err := c.propfind(path, true, |
|||
`<d:propfind xmlns:d='DAV:'> |
|||
<d:prop> |
|||
<d:displayname/> |
|||
<d:resourcetype/> |
|||
<d:getcontentlength/> |
|||
<d:getcontenttype/> |
|||
<d:getetag/> |
|||
<d:getlastmodified/> |
|||
</d:prop> |
|||
</d:propfind>`, |
|||
&response{}, |
|||
parse) |
|||
|
|||
if err != nil { |
|||
if _, ok := err.(*os.PathError); !ok { |
|||
err = NewPathErrorErr("ReadDir", path, err) |
|||
} |
|||
} |
|||
return f, err |
|||
} |
|||
|
|||
// Remove removes a remote file
|
|||
func (c *Client) Remove(path string) error { |
|||
return c.RemoveAll(path) |
|||
} |
|||
|
|||
// RemoveAll removes remote files
|
|||
func (c *Client) RemoveAll(path string) error { |
|||
rs, err := c.req("DELETE", path, nil, nil) |
|||
if err != nil { |
|||
return NewPathError("Remove", path, 400) |
|||
} |
|||
err = rs.Body.Close() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 { |
|||
return nil |
|||
} |
|||
|
|||
return NewPathError("Remove", path, rs.StatusCode) |
|||
} |
|||
|
|||
// Mkdir makes a directory
|
|||
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) { |
|||
path = FixSlashes(path) |
|||
status, err := c.mkcol(path) |
|||
if err != nil { |
|||
return |
|||
} |
|||
if status == 201 { |
|||
return nil |
|||
} |
|||
|
|||
return NewPathError("Mkdir", path, status) |
|||
} |
|||
|
|||
// MkdirAll like mkdir -p, but for webdav
|
|||
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { |
|||
path = FixSlashes(path) |
|||
status, err := c.mkcol(path) |
|||
if err != nil { |
|||
return |
|||
} |
|||
if status == 201 { |
|||
return nil |
|||
} |
|||
if status == 409 { |
|||
paths := strings.Split(path, "/") |
|||
sub := "/" |
|||
for _, e := range paths { |
|||
if e == "" { |
|||
continue |
|||
} |
|||
sub += e + "/" |
|||
status, err = c.mkcol(sub) |
|||
if err != nil { |
|||
return |
|||
} |
|||
if status != 201 { |
|||
return NewPathError("MkdirAll", sub, status) |
|||
} |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
return NewPathError("MkdirAll", path, status) |
|||
} |
|||
|
|||
// Rename moves a file from A to B
|
|||
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error { |
|||
return c.copymove("MOVE", oldpath, newpath, overwrite) |
|||
} |
|||
|
|||
// Copy copies a file from A to B
|
|||
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error { |
|||
return c.copymove("COPY", oldpath, newpath, overwrite) |
|||
} |
|||
|
|||
// Read reads the contents of a remote file
|
|||
func (c *Client) Read(path string) ([]byte, error) { |
|||
var stream io.ReadCloser |
|||
var err error |
|||
|
|||
if stream, err = c.ReadStream(path); err != nil { |
|||
return nil, err |
|||
} |
|||
defer stream.Close() |
|||
|
|||
buf := new(bytes.Buffer) |
|||
_, err = buf.ReadFrom(stream) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
return buf.Bytes(), nil |
|||
} |
|||
|
|||
// ReadStream reads the stream for a given path
|
|||
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) |
|||
} |
|||
|
|||
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
|
|||
// utilizing HTTP Range Requests if the server supports it.
|
|||
// The range is expressed as offset from the start of the file and length, for example
|
|||
// offset=10, length=10 will return bytes 10 through 19.
|
|||
//
|
|||
// If the server does not support partial content requests and returns full content instead,
|
|||
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
|
|||
// to `length`.
|
|||
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) { |
|||
rs, err := c.req("GET", path, nil, func(r *http.Request) { |
|||
if length > 0 { |
|||
r.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) |
|||
} else { |
|||
r.Header.Add("Range", fmt.Sprintf("bytes=%d-", offset)) |
|||
} |
|||
}) |
|||
if err != nil { |
|||
return nil, NewPathErrorErr("ReadStreamRange", path, err) |
|||
} |
|||
|
|||
if rs.StatusCode == http.StatusPartialContent { |
|||
// server supported partial content, return as-is.
|
|||
return rs.Body, nil |
|||
} |
|||
|
|||
// server returned success, but did not support partial content, so we have the whole
|
|||
// stream in rs.Body
|
|||
if rs.StatusCode == 200 { |
|||
// discard first 'offset' bytes.
|
|||
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { |
|||
return nil, NewPathErrorErr("ReadStreamRange", path, err) |
|||
} |
|||
|
|||
// return a io.ReadCloser that is limited to `length` bytes.
|
|||
return &limitedReadCloser{rc: rs.Body, remaining: int(length)}, nil |
|||
} |
|||
|
|||
rs.Body.Close() |
|||
return nil, NewPathError("ReadStream", path, rs.StatusCode) |
|||
} |
|||
|
|||
// Write writes data to a given path
|
|||
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) { |
|||
s, err := c.put(path, bytes.NewReader(data)) |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
switch s { |
|||
|
|||
case 200, 201, 204: |
|||
return nil |
|||
|
|||
case 404, 409: |
|||
err = c.createParentCollection(path) |
|||
if err != nil { |
|||
return |
|||
} |
|||
|
|||
s, err = c.put(path, bytes.NewReader(data)) |
|||
if err != nil { |
|||
return |
|||
} |
|||
if s == 200 || s == 201 || s == 204 { |
|||
return |
|||
} |
|||
} |
|||
|
|||
return NewPathError("Write", path, s) |
|||
} |
|||
|
|||
// WriteStream writes a stream
|
|||
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) { |
|||
|
|||
err = c.createParentCollection(path) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
s, err := c.put(path, stream) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
switch s { |
|||
case 200, 201, 204: |
|||
return nil |
|||
|
|||
default: |
|||
return NewPathError("WriteStream", path, s) |
|||
} |
|||
} |
@ -0,0 +1,183 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"crypto/md5" |
|||
"crypto/rand" |
|||
"encoding/hex" |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"strings" |
|||
) |
|||
|
|||
// DigestAuth structure holds our credentials
|
|||
type DigestAuth struct { |
|||
user string |
|||
pw string |
|||
digestParts map[string]string |
|||
} |
|||
|
|||
// NewDigestAuth creates a new instance of our Digest Authenticator
|
|||
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) { |
|||
return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
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 |
|||
} |
|||
|
|||
// Verify checks for authentication issues and may trigger a re-authentication
|
|||
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 |
|||
} |
|||
|
|||
// Close cleans up all resources
|
|||
func (d *DigestAuth) Close() error { |
|||
return nil |
|||
} |
|||
|
|||
// Clone creates a copy of itself
|
|||
func (d *DigestAuth) Clone() Authenticator { |
|||
parts := make(map[string]string, len(d.digestParts)) |
|||
for k, v := range d.digestParts { |
|||
parts[k] = v |
|||
} |
|||
return &DigestAuth{user: d.user, pw: d.pw, digestParts: parts} |
|||
} |
|||
|
|||
// String toString
|
|||
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 |
|||
// These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop.
|
|||
|
|||
var ( |
|||
ha1 string |
|||
ha2 string |
|||
nonceCount = 00000001 |
|||
cnonce = getCnonce() |
|||
response string |
|||
) |
|||
|
|||
// 'ha1' value depends on value of "algorithm" field
|
|||
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, |
|||
), |
|||
) |
|||
} |
|||
|
|||
// 'ha2' value depends on value of "qop" field
|
|||
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"])) |
|||
} |
|||
} |
|||
|
|||
// 'response' value depends on value of "qop" field
|
|||
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 |
|||
} |
@ -0,0 +1,57 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"errors" |
|||
"fmt" |
|||
"os" |
|||
) |
|||
|
|||
// ErrAuthChanged must be returned from the Verify method as an error
|
|||
// to trigger a re-authentication / negotiation with a new authenticator.
|
|||
var ErrAuthChanged = errors.New("authentication failed, change algorithm") |
|||
|
|||
// ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects.
|
|||
var ErrTooManyRedirects = errors.New("stopped after 10 redirects") |
|||
|
|||
// StatusError implements error and wraps
|
|||
// an erroneous status code.
|
|||
type StatusError struct { |
|||
Status int |
|||
} |
|||
|
|||
func (se StatusError) Error() string { |
|||
return fmt.Sprintf("%d", se.Status) |
|||
} |
|||
|
|||
// IsErrCode returns true if the given error
|
|||
// is an os.PathError wrapping a StatusError
|
|||
// with the given status code.
|
|||
func IsErrCode(err error, code int) bool { |
|||
if pe, ok := err.(*os.PathError); ok { |
|||
se, ok := pe.Err.(StatusError) |
|||
return ok && se.Status == code |
|||
} |
|||
return false |
|||
} |
|||
|
|||
// IsErrNotFound is shorthand for IsErrCode
|
|||
// for status 404.
|
|||
func IsErrNotFound(err error) bool { |
|||
return IsErrCode(err, 404) |
|||
} |
|||
|
|||
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, |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
"time" |
|||
) |
|||
|
|||
// File is our structure for a given file
|
|||
type File struct { |
|||
path string |
|||
name string |
|||
contentType string |
|||
size int64 |
|||
modified time.Time |
|||
etag string |
|||
isdir bool |
|||
} |
|||
|
|||
// Path returns the full path of a file
|
|||
func (f File) Path() string { |
|||
return f.path |
|||
} |
|||
|
|||
// Name returns the name of a file
|
|||
func (f File) Name() string { |
|||
return f.name |
|||
} |
|||
|
|||
// ContentType returns the content type of a file
|
|||
func (f File) ContentType() string { |
|||
return f.contentType |
|||
} |
|||
|
|||
// Size returns the size of a file
|
|||
func (f File) Size() int64 { |
|||
return f.size |
|||
} |
|||
|
|||
// Mode will return the mode of a given file
|
|||
func (f File) Mode() os.FileMode { |
|||
// TODO check webdav perms
|
|||
if f.isdir { |
|||
return 0775 | os.ModeDir |
|||
} |
|||
|
|||
return 0664 |
|||
} |
|||
|
|||
// ModTime returns the modified time of a file
|
|||
func (f File) ModTime() time.Time { |
|||
return f.modified |
|||
} |
|||
|
|||
// ETag returns the ETag of a file
|
|||
func (f File) ETag() string { |
|||
return f.etag |
|||
} |
|||
|
|||
// IsDir let us see if a given file is a directory or not
|
|||
func (f File) IsDir() bool { |
|||
return f.isdir |
|||
} |
|||
|
|||
// Sys ????
|
|||
func (f File) Sys() interface{} { |
|||
return nil |
|||
} |
|||
|
|||
// String lets us see file information
|
|||
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) |
|||
} |
@ -0,0 +1,229 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"fmt" |
|||
"godo/libs" |
|||
"io" |
|||
"net/http" |
|||
|
|||
"github.com/gorilla/mux" |
|||
) |
|||
|
|||
var client *Client |
|||
|
|||
func InitWebdav() error { |
|||
clientInfo, ok := libs.GetConfig("webdavClient") |
|||
if !ok { |
|||
return fmt.Errorf("failed to find configuration for webdavClient") |
|||
} |
|||
|
|||
infoMap, ok := clientInfo.(map[string]string) |
|||
if !ok { |
|||
return fmt.Errorf("configuration for webdavClient is not a map[string]string") |
|||
} |
|||
|
|||
url, ok := infoMap["url"] |
|||
if !ok { |
|||
return fmt.Errorf("missing 'url' in webdavClient configuration") |
|||
} |
|||
|
|||
username, ok := infoMap["username"] |
|||
if !ok { |
|||
return fmt.Errorf("missing 'username' in webdavClient configuration") |
|||
} |
|||
|
|||
password, ok := infoMap["password"] |
|||
if !ok { |
|||
return fmt.Errorf("missing 'password' in webdavClient configuration") |
|||
} |
|||
|
|||
client = NewClient(url, username, password) |
|||
|
|||
if err := client.Connect(); err != nil { |
|||
return fmt.Errorf("failed to connect to WebDAV server: %v", err) |
|||
} |
|||
return nil |
|||
} |
|||
func HandlePing(w http.ResponseWriter, r *http.Request) { |
|||
err := client.Connect() |
|||
if err != nil { |
|||
w.WriteHeader(http.StatusInternalServerError) |
|||
w.Write([]byte(err.Error())) |
|||
return |
|||
} |
|||
w.WriteHeader(http.StatusOK) |
|||
} |
|||
|
|||
// HandleReadDir: 读取目录内容
|
|||
func HandleReadDir(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
files, err := client.ReadDir(path) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
for _, f := range files { |
|||
w.Write([]byte(f.Name() + "\n")) |
|||
} |
|||
} |
|||
|
|||
// HandleStat: 获取文件状态
|
|||
func HandleStat(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
fi, err := client.Stat(path) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
fileInfo, ok := fi.(*File) |
|||
if !ok { |
|||
http.Error(w, "Unexpected file info type", http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 直接调用 fi.String(),因为 fi 应该是 *File 类型
|
|||
w.Write([]byte(fileInfo.String())) |
|||
} |
|||
|
|||
// HandleChmod: 改变文件权限 (不支持)
|
|||
func HandleChmod(w http.ResponseWriter, r *http.Request) { |
|||
http.Error(w, "Not supported", http.StatusNotImplemented) |
|||
} |
|||
|
|||
// HandleExists: 检查文件是否存在
|
|||
func HandleExists(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
_, err := client.Stat(path) |
|||
if err != nil { |
|||
http.Error(w, "File does not exist", http.StatusNotFound) |
|||
return |
|||
} |
|||
w.Write([]byte("File exists")) |
|||
} |
|||
|
|||
// HandleReadFile: 读取文件内容
|
|||
func HandleReadFile(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
data, err := client.Read(path) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write(data) |
|||
} |
|||
|
|||
// HandleUnlink: 删除文件
|
|||
func HandleUnlink(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
err := client.Remove(path) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("File deleted")) |
|||
} |
|||
|
|||
// HandleClear: 清空目录 (不支持)
|
|||
func HandleClear(w http.ResponseWriter, r *http.Request) { |
|||
http.Error(w, "Not supported", http.StatusNotImplemented) |
|||
} |
|||
|
|||
// HandleRename: 重命名文件或目录
|
|||
func HandleRename(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
oldPath := vars["oldPath"] |
|||
newPath := vars["newPath"] |
|||
err := client.Rename(oldPath, newPath, false) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("File renamed")) |
|||
} |
|||
|
|||
// HandleMkdir: 创建目录
|
|||
func HandleMkdir(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
err := client.Mkdir(path, 0) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("Directory created")) |
|||
} |
|||
|
|||
// HandleRmdir: 删除目录
|
|||
func HandleRmdir(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
err := client.RemoveAll(path) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("Directory removed")) |
|||
} |
|||
|
|||
// HandleCopyFile: 复制文件
|
|||
func HandleCopyFile(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
oldPath := vars["oldPath"] |
|||
newPath := vars["newPath"] |
|||
err := client.Copy(oldPath, newPath, false) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("File copied")) |
|||
} |
|||
|
|||
// HandleWriteFile: 写入文件
|
|||
func HandleWriteFile(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
body, err := io.ReadAll(r.Body) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
err = client.Write(path, body, 0) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("File written")) |
|||
} |
|||
|
|||
// HandleAppendFile: 追加到文件
|
|||
func HandleAppendFile(w http.ResponseWriter, r *http.Request) { |
|||
vars := mux.Vars(r) |
|||
path := vars["path"] |
|||
body, err := io.ReadAll(r.Body) |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 读取现有文件内容
|
|||
existingContent, err := client.Read(path) |
|||
if err != nil && !IsErrNotFound(err) { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
// 如果文件不存在,则创建新文件
|
|||
if IsErrNotFound(err) { |
|||
err = client.Write(path, body, 0) |
|||
} else { |
|||
// 如果文件存在,则追加内容
|
|||
err = client.Write(path, append(existingContent, body...), 0) |
|||
} |
|||
if err != nil { |
|||
http.Error(w, err.Error(), http.StatusInternalServerError) |
|||
return |
|||
} |
|||
w.Write([]byte("File appended")) |
|||
} |
@ -0,0 +1,54 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"bufio" |
|||
"fmt" |
|||
"net/url" |
|||
"os" |
|||
"regexp" |
|||
"strings" |
|||
) |
|||
|
|||
func parseLine(s string) (login, pass string) { |
|||
fields := strings.Fields(s) |
|||
for i, f := range fields { |
|||
if f == "login" { |
|||
login = fields[i+1] |
|||
} |
|||
if f == "password" { |
|||
pass = fields[i+1] |
|||
} |
|||
} |
|||
return login, pass |
|||
} |
|||
|
|||
// ReadConfig reads login and password configuration from ~/.netrc
|
|||
// machine foo.com login username password 123456
|
|||
func ReadConfig(uri, netrc string) (string, string) { |
|||
u, err := url.Parse(uri) |
|||
if err != nil { |
|||
return "", "" |
|||
} |
|||
|
|||
file, err := os.Open(netrc) |
|||
if err != nil { |
|||
return "", "" |
|||
} |
|||
defer file.Close() |
|||
|
|||
re := fmt.Sprintf(`^.*machine %s.*$`, u.Host) |
|||
scanner := bufio.NewScanner(file) |
|||
for scanner.Scan() { |
|||
s := scanner.Text() |
|||
|
|||
matched, err := regexp.MatchString(re, s) |
|||
if err != nil { |
|||
return "", "" |
|||
} |
|||
if matched { |
|||
return parseLine(s) |
|||
} |
|||
} |
|||
|
|||
return "", "" |
|||
} |
@ -0,0 +1,181 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"net/url" |
|||
"strings" |
|||
) |
|||
|
|||
// PassportAuth structure holds our credentials
|
|||
type PassportAuth struct { |
|||
user string |
|||
pw string |
|||
cookies []http.Cookie |
|||
inhibitRedirect bool |
|||
} |
|||
|
|||
// constructor for PassportAuth creates a new PassportAuth object and
|
|||
// automatically authenticates against the given partnerURL
|
|||
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 |
|||
} |
|||
|
|||
// Authorize the current request
|
|||
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error { |
|||
// prevent redirects to detect subsequent authentication requests
|
|||
if p.inhibitRedirect { |
|||
rq.Header.Set(XInhibitRedirect, "1") |
|||
} else { |
|||
p.inhibitRedirect = true |
|||
} |
|||
for _, cookie := range p.cookies { |
|||
rq.AddCookie(&cookie) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// Verify verifies if the authentication is good
|
|||
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") != "" { |
|||
// re-authentication required as we are redirected to the login page
|
|||
err = p.genCookies(c, rs.Request.URL.String(), &rs.Header) |
|||
} else { |
|||
// just a redirect, follow it
|
|||
p.inhibitRedirect = false |
|||
} |
|||
case 401: |
|||
err = NewPathError("Authorize", path, rs.StatusCode) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// Close cleans up all resources
|
|||
func (p *PassportAuth) Close() error { |
|||
return nil |
|||
} |
|||
|
|||
// Clone creates a Copy of itself
|
|||
func (p *PassportAuth) Clone() Authenticator { |
|||
// create a copy to allow independent cookie updates
|
|||
clonedCookies := make([]http.Cookie, len(p.cookies)) |
|||
copy(clonedCookies, p.cookies) |
|||
|
|||
return &PassportAuth{ |
|||
user: p.user, |
|||
pw: p.pw, |
|||
cookies: clonedCookies, |
|||
inhibitRedirect: true, |
|||
} |
|||
} |
|||
|
|||
// String toString
|
|||
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 { |
|||
// For more details refer to:
|
|||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pass/2c80637d-438c-4d4b-adc5-903170a779f3
|
|||
// Skipping step 1 and 2 as we already have the partner server challenge
|
|||
|
|||
baseAuthenticationServer := header.Get("Location") |
|||
baseAuthenticationServerURL, err := url.Parse(baseAuthenticationServer) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Skipping step 3 and 4 as we already know that we need and have the user's credentials
|
|||
// Step 5 (Sign-in request)
|
|||
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) |
|||
} |
|||
|
|||
// Step 6 (Token Response from Authentication Server)
|
|||
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) |
|||
} |
|||
|
|||
// Step 7 (First Authentication Request to Partner Server)
|
|||
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) |
|||
} |
|||
|
|||
// Step 8 (Set Token Message from Partner Server)
|
|||
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 |
|||
} |
@ -0,0 +1,181 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"io" |
|||
"log" |
|||
"net/http" |
|||
"path" |
|||
"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 { // TODO auth.continue() strategy(true|n times|until)?
|
|||
if r, err = http.NewRequest(method, uri, body); err != nil { |
|||
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 c.interceptor != nil { |
|||
c.interceptor(method, r) |
|||
} |
|||
|
|||
if rs, err = c.c.Do(r); err != nil { |
|||
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) mkcol(path string) (status int, err error) { |
|||
rs, err := c.req("MKCOL", path, nil, nil) |
|||
if err != nil { |
|||
return |
|||
} |
|||
defer rs.Body.Close() |
|||
|
|||
status = rs.StatusCode |
|||
if status == 405 { |
|||
status = 201 |
|||
} |
|||
|
|||
return |
|||
} |
|||
|
|||
func (c *Client) options(path string) (*http.Response, error) { |
|||
return c.req("OPTIONS", path, nil, func(rq *http.Request) { |
|||
rq.Header.Add("Depth", "0") |
|||
}) |
|||
} |
|||
|
|||
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) doCopyMove( |
|||
method string, |
|||
oldpath string, |
|||
newpath string, |
|||
overwrite bool, |
|||
) ( |
|||
status int, |
|||
r io.ReadCloser, |
|||
err error, |
|||
) { |
|||
rs, err := c.req(method, oldpath, nil, func(rq *http.Request) { |
|||
rq.Header.Add("Destination", PathEscape(Join(c.root, newpath))) |
|||
if overwrite { |
|||
rq.Header.Add("Overwrite", "T") |
|||
} else { |
|||
rq.Header.Add("Overwrite", "F") |
|||
} |
|||
}) |
|||
if err != nil { |
|||
return |
|||
} |
|||
status = rs.StatusCode |
|||
r = rs.Body |
|||
return |
|||
} |
|||
|
|||
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) { |
|||
s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite) |
|||
if err != nil { |
|||
return |
|||
} |
|||
if data != nil { |
|||
defer data.Close() |
|||
} |
|||
|
|||
switch s { |
|||
case 201, 204: |
|||
return nil |
|||
|
|||
case 207: |
|||
// TODO handle multistat errors, worst case ...
|
|||
log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data)) |
|||
|
|||
case 409: |
|||
err := c.createParentCollection(newpath) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return c.copymove(method, oldpath, newpath, overwrite) |
|||
} |
|||
|
|||
return NewPathError(method, oldpath, s) |
|||
} |
|||
|
|||
func (c *Client) put(path string, stream io.Reader) (status int, err error) { |
|||
rs, err := c.req("PUT", path, stream, nil) |
|||
if err != nil { |
|||
return |
|||
} |
|||
defer rs.Body.Close() |
|||
|
|||
status = rs.StatusCode |
|||
return |
|||
} |
|||
|
|||
func (c *Client) createParentCollection(itemPath string) (err error) { |
|||
parentPath := path.Dir(itemPath) |
|||
if parentPath == "." || parentPath == "/" { |
|||
return nil |
|||
} |
|||
|
|||
return c.MkdirAll(parentPath, 0755) |
|||
} |
@ -0,0 +1,113 @@ |
|||
package webdav |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/xml" |
|||
"io" |
|||
"net/url" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
) |
|||
|
|||
// PathEscape escapes all segments of a given path
|
|||
func PathEscape(path string) string { |
|||
s := strings.Split(path, "/") |
|||
for i, e := range s { |
|||
s[i] = url.PathEscape(e) |
|||
} |
|||
return strings.Join(s, "/") |
|||
} |
|||
|
|||
// FixSlash appends a trailing / to our string
|
|||
func FixSlash(s string) string { |
|||
if !strings.HasSuffix(s, "/") { |
|||
s += "/" |
|||
} |
|||
return s |
|||
} |
|||
|
|||
// FixSlashes appends and prepends a / if they are missing
|
|||
func FixSlashes(s string) string { |
|||
if !strings.HasPrefix(s, "/") { |
|||
s = "/" + s |
|||
} |
|||
|
|||
return FixSlash(s) |
|||
} |
|||
|
|||
// Join joins two paths
|
|||
func Join(path0 string, path1 string) string { |
|||
return strings.TrimSuffix(path0, "/") + "/" + strings.TrimPrefix(path1, "/") |
|||
} |
|||
|
|||
// String pulls a string out of our io.Reader
|
|||
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 parseUint(s *string) uint { |
|||
if n, e := strconv.ParseUint(*s, 10, 32); e == nil { |
|||
return uint(n) |
|||
} |
|||
return 0 |
|||
} |
|||
|
|||
func parseInt64(s *string) int64 { |
|||
if n, e := strconv.ParseInt(*s, 10, 64); e == nil { |
|||
return n |
|||
} |
|||
return 0 |
|||
} |
|||
|
|||
func parseModified(s *string) time.Time { |
|||
if t, e := time.Parse(time.RFC1123, *s); e == nil { |
|||
return t |
|||
} |
|||
return time.Unix(0, 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 |
|||
} |
|||
|
|||
// limitedReadCloser wraps a io.ReadCloser and limits the number of bytes that can be read from it.
|
|||
type limitedReadCloser struct { |
|||
rc io.ReadCloser |
|||
remaining int |
|||
} |
|||
|
|||
func (l *limitedReadCloser) Read(buf []byte) (int, error) { |
|||
if l.remaining <= 0 { |
|||
return 0, io.EOF |
|||
} |
|||
|
|||
if len(buf) > l.remaining { |
|||
buf = buf[0:l.remaining] |
|||
} |
|||
|
|||
n, err := l.rc.Read(buf) |
|||
l.remaining -= n |
|||
|
|||
return n, err |
|||
} |
|||
|
|||
func (l *limitedReadCloser) Close() error { |
|||
return l.rc.Close() |
|||
} |
Loading…
Reference in new issue