From f40fd83b82de01b17ffec9c8958effee0505173c Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Fri, 1 Dec 2023 11:44:50 -0500 Subject: [PATCH] initial import of gowebdav fork to support custom properties (#438) --- util/sync/webdav.go | 8 +- util/sync/webdavClient/LICENSE | 27 + util/sync/webdavClient/Makefile | 42 + util/sync/webdavClient/README.md | 962 ++++++++++++++++++++ util/sync/webdavClient/auth.go | 409 +++++++++ util/sync/webdavClient/auth_test.go | 62 ++ util/sync/webdavClient/basicAuth.go | 42 + util/sync/webdavClient/basicAuth_test.go | 51 ++ util/sync/webdavClient/client.go | 438 +++++++++ util/sync/webdavClient/client_test.go | 574 ++++++++++++ util/sync/webdavClient/digestAuth.go | 164 ++++ util/sync/webdavClient/digestAuth_test.go | 35 + util/sync/webdavClient/doc.go | 3 + util/sync/webdavClient/errors.go | 57 ++ util/sync/webdavClient/file.go | 77 ++ util/sync/webdavClient/netrc.go | 54 ++ util/sync/webdavClient/passportAuth.go | 181 ++++ util/sync/webdavClient/passportAuth_test.go | 66 ++ util/sync/webdavClient/requests.go | 181 ++++ util/sync/webdavClient/utils.go | 113 +++ util/sync/webdavClient/utils_test.go | 67 ++ 21 files changed, 3609 insertions(+), 4 deletions(-) create mode 100644 util/sync/webdavClient/LICENSE create mode 100644 util/sync/webdavClient/Makefile create mode 100644 util/sync/webdavClient/README.md create mode 100644 util/sync/webdavClient/auth.go create mode 100644 util/sync/webdavClient/auth_test.go create mode 100644 util/sync/webdavClient/basicAuth.go create mode 100644 util/sync/webdavClient/basicAuth_test.go create mode 100644 util/sync/webdavClient/client.go create mode 100644 util/sync/webdavClient/client_test.go create mode 100644 util/sync/webdavClient/digestAuth.go create mode 100644 util/sync/webdavClient/digestAuth_test.go create mode 100644 util/sync/webdavClient/doc.go create mode 100644 util/sync/webdavClient/errors.go create mode 100644 util/sync/webdavClient/file.go create mode 100644 util/sync/webdavClient/netrc.go create mode 100644 util/sync/webdavClient/passportAuth.go create mode 100644 util/sync/webdavClient/passportAuth_test.go create mode 100644 util/sync/webdavClient/requests.go create mode 100644 util/sync/webdavClient/utils.go create mode 100644 util/sync/webdavClient/utils_test.go diff --git a/util/sync/webdav.go b/util/sync/webdav.go index 840a685b..7a2b6ca3 100644 --- a/util/sync/webdav.go +++ b/util/sync/webdav.go @@ -1,8 +1,8 @@ package sync import ( + "github.com/openziti/zrok/util/sync/webdavClient" "github.com/pkg/errors" - "github.com/studio-b12/gowebdav" "io" "os" "path/filepath" @@ -16,11 +16,11 @@ type WebDAVTargetConfig struct { } type WebDAVTarget struct { - c *gowebdav.Client + c *webdavClient.Client } func NewWebDAVTarget(cfg *WebDAVTargetConfig) (*WebDAVTarget, error) { - c := gowebdav.NewClient(cfg.URL, cfg.Username, cfg.Password) + c := webdavClient.NewClient(cfg.URL, cfg.Username, cfg.Password) if err := c.Connect(); err != nil { return nil, errors.Wrap(err, "error connecting to webdav target") } @@ -48,7 +48,7 @@ func (t *WebDAVTarget) recurse(path string, tree []*Object) ([]*Object, error) { return nil, err } } else { - if v, ok := f.(gowebdav.File); ok { + if v, ok := f.(webdavClient.File); ok { tree = append(tree, &Object{ Path: filepath.ToSlash(filepath.Join(path, f.Name())), Size: v.Size(), diff --git a/util/sync/webdavClient/LICENSE b/util/sync/webdavClient/LICENSE new file mode 100644 index 00000000..a7cd4420 --- /dev/null +++ b/util/sync/webdavClient/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014, Studio B12 GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/util/sync/webdavClient/Makefile b/util/sync/webdavClient/Makefile new file mode 100644 index 00000000..48dbb4e7 --- /dev/null +++ b/util/sync/webdavClient/Makefile @@ -0,0 +1,42 @@ +BIN := gowebdav +SRC := $(wildcard *.go) cmd/gowebdav/main.go + +all: test cmd + +cmd: ${BIN} + +${BIN}: ${SRC} + go build -o $@ ./cmd/gowebdav + +test: + go test -modfile=go_test.mod -v -short -cover ./... + +api: .go/bin/godoc2md + @sed '/^## API$$/,$$d' -i README.md + @echo '## API' >> README.md + @$< github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\ + sed '2d' |\ + sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\ + sed 's/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\ + sed 's/^#/##/g' >> README.md + +check: .go/bin/gocyclo + gofmt -w -s $(SRC) + @echo + .go/bin/gocyclo -over 15 . + @echo + go vet -modfile=go_test.mod ./... + + +.go/bin/godoc2md: + @mkdir -p $(@D) + @GOPATH="$(CURDIR)/.go" go install github.com/davecheney/godoc2md@latest + +.go/bin/gocyclo: + @mkdir -p $(@D) + @GOPATH="$(CURDIR)/.go" go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + +clean: + @rm -f ${BIN} + +.PHONY: all cmd clean test api check diff --git a/util/sync/webdavClient/README.md b/util/sync/webdavClient/README.md new file mode 100644 index 00000000..6c9a795a --- /dev/null +++ b/util/sync/webdavClient/README.md @@ -0,0 +1,962 @@ +# GoWebDAV + +[![Unit Tests Status](https://github.com/studio-b12/gowebdav/actions/workflows/tests.yml/badge.svg)](https://github.com/studio-b12/gowebdav/actions/workflows/tests.yml) +[![Build Artifacts Status](https://github.com/studio-b12/gowebdav/actions/workflows/artifacts.yml/badge.svg)](https://github.com/studio-b12/gowebdav/actions/workflows/artifacts.yml) +[![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav) +[![Go Report Card](https://goreportcard.com/badge/github.com/studio-b12/gowebdav)](https://goreportcard.com/report/github.com/studio-b12/gowebdav) + +A pure Golang WebDAV client library that comes with +a [reference implementation](https://github.com/studio-b12/gowebdav/tree/master/cmd/gowebdav). + +## Features at a glance + +Our `gowebdav` library allows to perform following actions on the remote WebDAV server: + +* [create path](#create-path-on-a-webdav-server) +* [get files list](#get-files-list) +* [download file](#download-file-to-byte-array) +* [upload file](#upload-file-from-byte-array) +* [get information about specified file/folder](#get-information-about-specified-filefolder) +* [move file to another location](#move-file-to-another-location) +* [copy file to another location](#copy-file-to-another-location) +* [delete file](#delete-file) + +It also provides an [authentication API](#type-authenticator) that makes it easy to encapsulate and control complex +authentication challenges. +The default implementation negotiates the algorithm based on the user's preferences and the methods offered by the +remote server. + +Out-of-box authentication support for: + +* [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication) +* [DigestAuth](https://en.wikipedia.org/wiki/Digest_access_authentication) +* [MS-PASS](https://github.com/studio-b12/gowebdav/pull/70#issuecomment-1421713726) +* [WIP Kerberos](https://github.com/studio-b12/gowebdav/pull/71#issuecomment-1416465334) +* [WIP Bearer Token](https://github.com/studio-b12/gowebdav/issues/61) + +## Usage + +First of all you should create `Client` instance using `NewClient()` function: + +```go +root := "https://webdav.mydomain.me" +user := "user" +password := "password" + +c := gowebdav.NewClient(root, user, password) +c.Connect() +// kick of your work! +``` + +After you can use this `Client` to perform actions, described below. + +**NOTICE:** We will not check for errors in the examples, to focus you on the `gowebdav` library's code, but you should +do it in your code! + +### Create path on a WebDAV server + +```go +err := c.Mkdir("folder", 0644) +``` + +In case you want to create several folders you can use `c.MkdirAll()`: + +```go +err := c.MkdirAll("folder/subfolder/subfolder2", 0644) +``` + +### Get files list + +```go +files, _ := c.ReadDir("folder/subfolder") +for _, file := range files { + //notice that [file] has os.FileInfo type + fmt.Println(file.Name()) +} +``` + +### Download file to byte array + +```go +webdavFilePath := "folder/subfolder/file.txt" +localFilePath := "/tmp/webdav/file.txt" + +bytes, _ := c.Read(webdavFilePath) +os.WriteFile(localFilePath, bytes, 0644) +``` + +### Download file via reader + +Also you can use `c.ReadStream()` method: + +```go +webdavFilePath := "folder/subfolder/file.txt" +localFilePath := "/tmp/webdav/file.txt" + +reader, _ := c.ReadStream(webdavFilePath) + +file, _ := os.Create(localFilePath) +defer file.Close() + +io.Copy(file, reader) +``` + +### Upload file from byte array + +```go +webdavFilePath := "folder/subfolder/file.txt" +localFilePath := "/tmp/webdav/file.txt" + +bytes, _ := os.ReadFile(localFilePath) + +c.Write(webdavFilePath, bytes, 0644) +``` + +### Upload file via writer + +```go +webdavFilePath := "folder/subfolder/file.txt" +localFilePath := "/tmp/webdav/file.txt" + +file, _ := os.Open(localFilePath) +defer file.Close() + +c.WriteStream(webdavFilePath, file, 0644) +``` + +### Get information about specified file/folder + +```go +webdavFilePath := "folder/subfolder/file.txt" + +info := c.Stat(webdavFilePath) +//notice that [info] has os.FileInfo type +fmt.Println(info) +``` + +### Move file to another location + +```go +oldPath := "folder/subfolder/file.txt" +newPath := "folder/subfolder/moved.txt" +isOverwrite := true + +c.Rename(oldPath, newPath, isOverwrite) +``` + +### Copy file to another location + +```go +oldPath := "folder/subfolder/file.txt" +newPath := "folder/subfolder/file-copy.txt" +isOverwrite := true + +c.Copy(oldPath, newPath, isOverwrite) +``` + +### Delete file + +```go +webdavFilePath := "folder/subfolder/file.txt" + +c.Remove(webdavFilePath) +``` + +## Links + +More details about WebDAV server you can read from following resources: + +* [RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc4918) +* [RFC 5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)](https://tools.ietf.org/html/rfc5689) +* [RFC 2616 - HTTP/1.1 Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html "HTTP/1.1 Status Code Definitions") +* [WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseaul](https://books.google.de/books?isbn=0130652083 "WebDav: Next Generation Collaborative Web Authoring By Lisa Dusseault") + +**NOTICE**: RFC 2518 is obsoleted by RFC 4918 in June 2007 + +## Contributing + +All contributing are welcome. If you have any suggestions or find some bug - please create an Issue to let us make this +project better. We appreciate your help! + +## License + +This library is distributed under the BSD 3-Clause license found in +the [LICENSE](https://github.com/studio-b12/gowebdav/blob/master/LICENSE) file. + +## API + +`import "github.com/studio-b12/gowebdav"` + +* [Overview](#pkg-overview) +* [Index](#pkg-index) +* [Examples](#pkg-examples) +* [Subdirectories](#pkg-subdirectories) + +### Overview + +Package gowebdav is a WebDAV client library with a command line tool +included. + +### Index + +* [Constants](#pkg-constants) +* [Variables](#pkg-variables) +* [func FixSlash(s string) string](#FixSlash) +* [func FixSlashes(s string) string](#FixSlashes) +* [func IsErrCode(err error, code int) bool](#IsErrCode) +* [func IsErrNotFound(err error) bool](#IsErrNotFound) +* [func Join(path0 string, path1 string) string](#Join) +* [func NewPathError(op string, path string, statusCode int) error](#NewPathError) +* [func NewPathErrorErr(op string, path string, err error) error](#NewPathErrorErr) +* [func PathEscape(path string) string](#PathEscape) +* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig) +* [func String(r io.Reader) string](#String) +* [type AuthFactory](#AuthFactory) +* [type Authenticator](#Authenticator) + * [func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error)](#NewDigestAuth) + * [func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error)](#NewPassportAuth) +* [type Authorizer](#Authorizer) + * [func NewAutoAuth(login string, secret string) Authorizer](#NewAutoAuth) + * [func NewEmptyAuth() Authorizer](#NewEmptyAuth) + * [func NewPreemptiveAuth(auth Authenticator) Authorizer](#NewPreemptiveAuth) +* [type BasicAuth](#BasicAuth) + * [func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#BasicAuth.Authorize) + * [func (b *BasicAuth) Clone() Authenticator](#BasicAuth.Clone) + * [func (b *BasicAuth) Close() error](#BasicAuth.Close) + * [func (b *BasicAuth) String() string](#BasicAuth.String) + * [func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#BasicAuth.Verify) +* [type Client](#Client) + * [func NewAuthClient(uri string, auth Authorizer) *Client](#NewAuthClient) + * [func NewClient(uri, user, pw string) *Client](#NewClient) + * [func (c *Client) Connect() error](#Client.Connect) + * [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy) + * [func (c *Client) Mkdir(path string, _ os.FileMode) (err error)](#Client.Mkdir) + * [func (c *Client) MkdirAll(path string, _ os.FileMode) (err error)](#Client.MkdirAll) + * [func (c *Client) Read(path string) ([]byte, error)](#Client.Read) + * [func (c *Client) ReadDir(path string) ([]os.FileInfo, error)](#Client.ReadDir) + * [func (c *Client) ReadStream(path string) (io.ReadCloser, error)](#Client.ReadStream) + * [func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error)](#Client.ReadStreamRange) + * [func (c *Client) Remove(path string) error](#Client.Remove) + * [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll) + * [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename) + * [func (c *Client) SetHeader(key, value string)](#Client.SetHeader) + * [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor) + * [func (c *Client) SetJar(jar http.CookieJar)](#Client.SetJar) + * [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout) + * [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport) + * [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat) + * [func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error)](#Client.Write) + * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)](#Client.WriteStream) +* [type DigestAuth](#DigestAuth) + * [func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#DigestAuth.Authorize) + * [func (d *DigestAuth) Clone() Authenticator](#DigestAuth.Clone) + * [func (d *DigestAuth) Close() error](#DigestAuth.Close) + * [func (d *DigestAuth) String() string](#DigestAuth.String) + * [func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#DigestAuth.Verify) +* [type File](#File) + * [func (f File) ContentType() string](#File.ContentType) + * [func (f File) ETag() string](#File.ETag) + * [func (f File) IsDir() bool](#File.IsDir) + * [func (f File) ModTime() time.Time](#File.ModTime) + * [func (f File) Mode() os.FileMode](#File.Mode) + * [func (f File) Name() string](#File.Name) + * [func (f File) Path() string](#File.Path) + * [func (f File) Size() int64](#File.Size) + * [func (f File) String() string](#File.String) + * [func (f File) Sys() interface{}](#File.Sys) +* [type PassportAuth](#PassportAuth) + * [func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#PassportAuth.Authorize) + * [func (p *PassportAuth) Clone() Authenticator](#PassportAuth.Clone) + * [func (p *PassportAuth) Close() error](#PassportAuth.Close) + * [func (p *PassportAuth) String() string](#PassportAuth.String) + * [func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#PassportAuth.Verify) +* [type StatusError](#StatusError) + * [func (se StatusError) Error() string](#StatusError.Error) + +##### Examples + +* [PathEscape](#example_PathEscape) + +##### Package files + +[auth.go](https://github.com/studio-b12/gowebdav/blob/master/auth.go) [basicAuth.go](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go) [client.go](https://github.com/studio-b12/gowebdav/blob/master/client.go) [digestAuth.go](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go) [doc.go](https://github.com/studio-b12/gowebdav/blob/master/doc.go) [errors.go](https://github.com/studio-b12/gowebdav/blob/master/errors.go) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.go) [passportAuth.go](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go) + +### Constants + +``` go +const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" +``` + +### Variables + +``` go +var ErrAuthChanged = errors.New("authentication failed, change algorithm") +``` + +ErrAuthChanged must be returned from the Verify method as an error +to trigger a re-authentication / negotiation with a new authenticator. + +``` go +var ErrTooManyRedirects = errors.New("stopped after 10 redirects") +``` + +ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects. + +### func [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=354:384#L23) + +``` go +func FixSlash(s string) string +``` + +FixSlash appends a trailing / to our string + +### func [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:538#L31) + +``` go +func FixSlashes(s string) string +``` + +FixSlashes appends and prepends a / if they are missing + +### func [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=740:780#L29) + +``` go +func IsErrCode(err error, code int) bool +``` + +IsErrCode returns true if the given error +is an os.PathError wrapping a StatusError +with the given status code. + +### func [IsErrNotFound](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=972:1006#L39) + +``` go +func IsErrNotFound(err error) bool +``` + +IsErrNotFound is shorthand for IsErrCode +for status 404. + +### func [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=639:683#L40) + +``` go +func Join(path0 string, path1 string) string +``` + +Join joins two paths + +### func [NewPathError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1040:1103#L43) + +``` go +func NewPathError(op string, path string, statusCode int) error +``` + +### func [NewPathErrorErr](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1194:1255#L51) + +``` go +func NewPathErrorErr(op string, path string, err error) error +``` + +### func [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=153:188#L14) + +``` go +func PathEscape(path string) string +``` + +PathEscape escapes all segments of a given path + +### func [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27) + +``` go +func ReadConfig(uri, netrc string) (string, string) +``` + +ReadConfig reads login and password configuration from ~/.netrc +machine foo.com login username password 123456 + +### func [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=813:844#L45) + +``` go +func String(r io.Reader) string +``` + +String pulls a string out of our io.Reader + +### type [AuthFactory](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=150:251#L13) + +``` go +type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) +``` + +AuthFactory prototype function to create a new Authenticator + +### type [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=2155:2695#L56) + +``` go +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 +} +``` + +A 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. + +#### func [NewDigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=324:406#L21) + +``` go +func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) +``` + +NewDigestAuth creates a new instance of our Digest Authenticator + +#### func [NewPassportAuth](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=386:495#L21) + +``` go +func NewPassportAuth(c *http.Client, user, pw, partnerURL string, header *http.Header) (Authenticator, error) +``` + +constructor for PassportAuth creates a new PassportAuth object and +automatically authenticates against the given partnerURL + +### type [Authorizer](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=349:764#L17) + +``` go +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) +} +``` + +Authorizer our Authenticator factory which creates an +`Authenticator` per action/request. + +#### func [NewAutoAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=3789:3845#L109) + +``` go +func NewAutoAuth(login string, secret string) Authorizer +``` + +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 [NewEmptyAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=4694:4724#L132) + +``` go +func NewEmptyAuth() Authorizer +``` + +NewEmptyAuth creates an empty Authenticator factory +The order of adding the Authenticator matters. +First In, First Out. +It offers the `NewAutoAuth` features. + +#### func [NewPreemptiveAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=5300:5353#L148) + +``` go +func NewPreemptiveAuth(auth Authenticator) Authorizer +``` + +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. + +### type [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L9) + +``` go +type BasicAuth struct { + // contains filtered or unexported fields +} + +``` + +BasicAuth structure holds our credentials + +#### func (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=180:262#L15) + +``` go +func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error +``` + +Authorize the current request + +#### func (\*BasicAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=666:707#L34) + +``` go +func (b *BasicAuth) Clone() Authenticator +``` + +Clone creates a Copy of itself + +#### func (\*BasicAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=581:614#L29) + +``` go +func (b *BasicAuth) Close() error +``` + +Close cleans up all resources + +#### func (\*BasicAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=778:813#L40) + +``` go +func (b *BasicAuth) String() string +``` + +String toString + +#### func (\*BasicAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=352:449#L21) + +``` go +func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) +``` + +Verify verifies if the authentication + +### type [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=220:388#L19) + +``` go +type Client struct { + // contains filtered or unexported fields +} + +``` + +Client defines our structure + +#### func [NewAuthClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=608:663#L33) + +``` go +func NewAuthClient(uri string, auth Authorizer) *Client +``` + +NewAuthClient creates a new client instance with a custom Authorizer + +#### func [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=436:480#L28) + +``` go +func NewClient(uri, user, pw string) *Client +``` + +NewClient creates a new instance of client + +#### func (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1829:1861#L74) + +``` go +func (c *Client) Connect() error +``` + +Connect connects to our dav server + +#### func (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6815:6883#L310) + +``` go +func (c *Client) Copy(oldpath, newpath string, overwrite bool) error +``` + +Copy copies a file from A to B + +#### func (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5790:5852#L259) + +``` go +func (c *Client) Mkdir(path string, _ os.FileMode) (err error) +``` + +Mkdir makes a directory + +#### func (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6065:6130#L273) + +``` go +func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) +``` + +MkdirAll like mkdir -p, but for webdav + +#### func (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6989:7039#L315) + +``` go +func (c *Client) Read(path string) ([]byte, error) +``` + +Read reads the contents of a remote file + +#### func (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2855:2915#L117) + +``` go +func (c *Client) ReadDir(path string) ([]os.FileInfo, error) +``` + +ReadDir reads the contents of a remote directory + +#### func (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7350:7413#L333) + +``` go +func (c *Client) ReadStream(path string) (io.ReadCloser, error) +``` + +ReadStream reads the stream for a given path + +#### func (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8162:8252#L355) + +``` go +func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) +``` + +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 (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5296:5338#L236) + +``` go +func (c *Client) Remove(path string) error +``` + +Remove removes a remote file + +#### func (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5404:5449#L241) + +``` go +func (c *Client) RemoveAll(path string) error +``` + +RemoveAll removes remote files + +#### func (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6649:6719#L305) + +``` go +func (c *Client) Rename(oldpath, newpath string, overwrite bool) error +``` + +Rename moves a file from A to B + +#### func (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1092:1137#L49) + +``` go +func (c *Client) SetHeader(key, value string) +``` + +SetHeader lets us set arbitrary headers for a given client + +#### func (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1326#L54) + +``` go +func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) +``` + +SetInterceptor lets us set an arbitrary interceptor for a given client + +#### func (\*Client) [SetJar](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1727:1770#L69) + +``` go +func (c *Client) SetJar(jar http.CookieJar) +``` + +SetJar exposes the ability to set a cookie jar to the client. + +#### func (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1428:1478#L59) + +``` go +func (c *Client) SetTimeout(timeout time.Duration) +``` + +SetTimeout exposes the ability to set a time limit for requests + +#### func (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1629#L64) + +``` go +func (c *Client) SetTransport(transport http.RoundTripper) +``` + +SetTransport exposes the ability to define custom transports + +#### func (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4241:4296#L184) + +``` go +func (c *Client) Stat(path string) (os.FileInfo, error) +``` + +Stat returns the file stats for a specified path + +#### func (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9272:9347#L389) + +``` go +func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) +``` + +Write writes data to a given path + +#### func (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9771:9857#L419) + +``` go +func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) +``` + +WriteStream writes a stream + +### type [DigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=157:254#L14) + +``` go +type DigestAuth struct { + // contains filtered or unexported fields +} + +``` + +DigestAuth structure holds our credentials + +#### func (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=525:608#L26) + +``` go +func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error +``` + +Authorize the current request + +#### func (\*DigestAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1228:1270#L49) + +``` go +func (d *DigestAuth) Clone() Authenticator +``` + +Clone creates a copy of itself + +#### func (\*DigestAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1142:1176#L44) + +``` go +func (d *DigestAuth) Close() error +``` + +Close cleans up all resources + +#### func (\*DigestAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1466:1502#L58) + +``` go +func (d *DigestAuth) String() string +``` + +String toString + +#### func (\*DigestAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=912:1010#L36) + +``` go +func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) +``` + +Verify checks for authentication issues and may trigger a re-authentication + +### type [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10) + +``` go +type File struct { + // contains filtered or unexported fields +} + +``` + +File is our structure for a given file + +#### func (File) [ContentType](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=476:510#L31) + +``` go +func (f File) ContentType() string +``` + +ContentType returns the content type of a file + +#### func (File) [ETag](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=929:956#L56) + +``` go +func (f File) ETag() string +``` + +ETag returns the ETag of a file + +#### func (File) [IsDir](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1035:1061#L61) + +``` go +func (f File) IsDir() bool +``` + +IsDir let us see if a given file is a directory or not + +#### func (File) [ModTime](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=836:869#L51) + +``` go +func (f File) ModTime() time.Time +``` + +ModTime returns the modified time of a file + +#### func (File) [Mode](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=665:697#L41) + +``` go +func (f File) Mode() os.FileMode +``` + +Mode will return the mode of a given file + +#### func (File) [Name](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=378:405#L26) + +``` go +func (f File) Name() string +``` + +Name returns the name of a file + +#### func (File) [Path](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=295:322#L21) + +``` go +func (f File) Path() string +``` + +Path returns the full path of a file + +#### func (File) [Size](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=573:599#L36) + +``` go +func (f File) Size() int64 +``` + +Size returns the size of a file + +#### func (File) [String](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1183:1212#L71) + +``` go +func (f File) String() string +``` + +String lets us see file information + +#### func (File) [Sys](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=1095:1126#L66) + +``` go +func (f File) Sys() interface{} +``` + +Sys ???? + +### type [PassportAuth](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=125:254#L12) + +``` go +type PassportAuth struct { + // contains filtered or unexported fields +} + +``` + +PassportAuth structure holds our credentials + +#### func (\*PassportAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=690:775#L32) + +``` go +func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error +``` + +Authorize the current request + +#### func (\*PassportAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1701:1745#L69) + +``` go +func (p *PassportAuth) Clone() Authenticator +``` + +Clone creates a Copy of itself + +#### func (\*PassportAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1613:1649#L64) + +``` go +func (p *PassportAuth) Close() error +``` + +Close cleans up all resources + +#### func (\*PassportAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=2048:2086#L83) + +``` go +func (p *PassportAuth) String() string +``` + +String toString + +#### func (\*PassportAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1075:1175#L46) + +``` go +func (p *PassportAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) +``` + +Verify verifies if the authentication is good + +### type [StatusError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=499:538#L18) + +``` go +type StatusError struct { + Status int +} + +``` + +StatusError implements error and wraps +an erroneous status code. + +#### func (StatusError) [Error](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=540:576#L22) + +``` go +func (se StatusError) Error() string +``` + +- - - +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/util/sync/webdavClient/auth.go b/util/sync/webdavClient/auth.go new file mode 100644 index 00000000..32d76168 --- /dev/null +++ b/util/sync/webdavClient/auth.go @@ -0,0 +1,409 @@ +package webdavClient + +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) +} + +// A 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 = 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") +} diff --git a/util/sync/webdavClient/auth_test.go b/util/sync/webdavClient/auth_test.go new file mode 100644 index 00000000..d12c0f0d --- /dev/null +++ b/util/sync/webdavClient/auth_test.go @@ -0,0 +1,62 @@ +package webdavClient + +import ( + "bytes" + "net/http" + "strings" + "testing" +) + +func TestEmptyAuth(t *testing.T) { + auth := NewEmptyAuth() + srv, _, _ := newAuthSrv(t, basicAuth) + defer srv.Close() + cli := NewAuthClient(srv.URL, auth) + if err := cli.Connect(); err == nil { + t.Fatalf("got nil want error") + } +} + +func TestRedirectAuthWIP(t *testing.T) { + hasPassedAuthServer := false + authHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if user, passwd, ok := r.BasicAuth(); ok { + if user == "user" && passwd == "password" { + hasPassedAuthServer = true + w.WriteHeader(200) + return + } + } + w.Header().Set("Www-Authenticate", `Basic realm="x"`) + w.WriteHeader(401) + } + } + + psrv, _, _ := newAuthSrv(t, authHandler) + defer psrv.Close() + + dataHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + hasAuth := strings.Contains(r.Header.Get("Authorization"), "Basic dXNlcjpwYXNzd29yZA==") + + if hasPassedAuthServer && hasAuth { + h.ServeHTTP(w, r) + return + } + w.Header().Set("Www-Authenticate", `Basic realm="x"`) + http.Redirect(w, r, psrv.URL+"/", 302) + } + } + + srv, _, _ := newAuthSrv(t, dataHandler) + defer srv.Close() + cli := NewClient(srv.URL, "user", "password") + data, err := cli.Read("/hello.txt") + if err != nil { + t.Logf("WIP got error=%v; want nil", err) + } + if bytes.Compare(data, []byte("hello gowebdav\n")) != 0 { + t.Logf("WIP got data=%v; want=hello gowebdav", data) + } +} diff --git a/util/sync/webdavClient/basicAuth.go b/util/sync/webdavClient/basicAuth.go new file mode 100644 index 00000000..10e5b8f4 --- /dev/null +++ b/util/sync/webdavClient/basicAuth.go @@ -0,0 +1,42 @@ +package webdavClient + +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) +} diff --git a/util/sync/webdavClient/basicAuth_test.go b/util/sync/webdavClient/basicAuth_test.go new file mode 100644 index 00000000..3a62713a --- /dev/null +++ b/util/sync/webdavClient/basicAuth_test.go @@ -0,0 +1,51 @@ +package webdavClient + +import ( + "net/http" + "testing" +) + +func TestNewBasicAuth(t *testing.T) { + a := &BasicAuth{user: "user", pw: "password"} + + ex := "BasicAuth login: user" + if a.String() != ex { + t.Error("expected: " + ex + " got: " + a.String()) + } + + if a.Clone() != a { + t.Error("expected the same instance") + } + + if a.Close() != nil { + t.Error("expected close without errors") + } +} + +func TestBasicAuthAuthorize(t *testing.T) { + a := &BasicAuth{user: "user", pw: "password"} + rq, _ := http.NewRequest("GET", "http://localhost/", nil) + a.Authorize(nil, rq, "/") + if rq.Header.Get("Authorization") != "Basic dXNlcjpwYXNzd29yZA==" { + t.Error("got wrong Authorization header: " + rq.Header.Get("Authorization")) + } +} + +func TestPreemtiveBasicAuth(t *testing.T) { + a := &BasicAuth{user: "user", pw: "password"} + auth := NewPreemptiveAuth(a) + n, b := auth.NewAuthenticator(nil) + if b != nil { + t.Error("expected body to be nil") + } + if n != a { + t.Error("expected the same instance") + } + + srv, _, _ := newAuthSrv(t, basicAuth) + defer srv.Close() + cli := NewAuthClient(srv.URL, auth) + if err := cli.Connect(); err != nil { + t.Fatalf("got error: %v, want nil", err) + } +} diff --git a/util/sync/webdavClient/client.go b/util/sync/webdavClient/client.go new file mode 100644 index 00000000..fa806c5a --- /dev/null +++ b/util/sync/webdavClient/client.go @@ -0,0 +1,438 @@ +package webdavClient + +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, + ` + + + + + + + + + `, + &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, + ` + + + + + + + + + `, + &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) + } +} diff --git a/util/sync/webdavClient/client_test.go b/util/sync/webdavClient/client_test.go new file mode 100644 index 00000000..8d60477a --- /dev/null +++ b/util/sync/webdavClient/client_test.go @@ -0,0 +1,574 @@ +package webdavClient + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync" + "testing" + "time" + + "golang.org/x/net/webdav" +) + +func noAuthHndl(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } +} + +func basicAuth(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if user, passwd, ok := r.BasicAuth(); ok { + if user == "user" && passwd == "password" { + h.ServeHTTP(w, r) + return + } + + http.Error(w, "not authorized", 403) + } else { + w.Header().Set("WWW-Authenticate", `Basic realm="x"`) + w.WriteHeader(401) + } + } +} + +func multipleAuth(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + notAuthed := false + if r.Header.Get("Authorization") == "" { + notAuthed = true + } else if user, passwd, ok := r.BasicAuth(); ok { + if user == "user" && passwd == "password" { + h.ServeHTTP(w, r) + return + } + notAuthed = true + } else if strings.HasPrefix(r.Header.Get("Authorization"), "Digest ") { + pairs := strings.TrimPrefix(r.Header.Get("Authorization"), "Digest ") + digestParts := make(map[string]string) + for _, pair := range strings.Split(pairs, ",") { + kv := strings.SplitN(strings.TrimSpace(pair), "=", 2) + key, value := kv[0], kv[1] + value = strings.Trim(value, `"`) + digestParts[key] = value + } + if digestParts["qop"] == "" { + digestParts["qop"] = "auth" + } + + ha1 := getMD5(fmt.Sprint(digestParts["username"], ":", digestParts["realm"], ":", "digestPW")) + ha2 := getMD5(fmt.Sprint(r.Method, ":", digestParts["uri"])) + expected := getMD5(fmt.Sprint(ha1, + ":", digestParts["nonce"], + ":", digestParts["nc"], + ":", digestParts["cnonce"], + ":", digestParts["qop"], + ":", ha2)) + + if expected == digestParts["response"] { + h.ServeHTTP(w, r) + return + } + notAuthed = true + } + + if notAuthed { + w.Header().Add("WWW-Authenticate", `Digest realm="testrealm@host.com", qop="auth,auth-int",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",opaque="5ccc069c403ebaf9f0171e9517f40e41"`) + w.Header().Add("WWW-Authenticate", `Basic realm="x"`) + w.WriteHeader(401) + } + } +} + +func fillFs(t *testing.T, fs webdav.FileSystem) context.Context { + ctx := context.Background() + f, err := fs.OpenFile(ctx, "hello.txt", os.O_CREATE, 0644) + if err != nil { + t.Errorf("fail to crate file: %v", err) + } + f.Write([]byte("hello gowebdav\n")) + f.Close() + err = fs.Mkdir(ctx, "/test", 0755) + if err != nil { + t.Errorf("fail to crate directory: %v", err) + } + f, err = fs.OpenFile(ctx, "/test/test.txt", os.O_CREATE, 0644) + if err != nil { + t.Errorf("fail to crate file: %v", err) + } + f.Write([]byte("test test gowebdav\n")) + f.Close() + return ctx +} + +func newServer(t *testing.T) (*Client, *httptest.Server, webdav.FileSystem, context.Context) { + return newAuthServer(t, basicAuth) +} + +func newAuthServer(t *testing.T, auth func(h http.Handler) http.HandlerFunc) (*Client, *httptest.Server, webdav.FileSystem, context.Context) { + srv, fs, ctx := newAuthSrv(t, auth) + cli := NewClient(srv.URL, "user", "password") + return cli, srv, fs, ctx +} + +func newAuthSrv(t *testing.T, auth func(h http.Handler) http.HandlerFunc) (*httptest.Server, webdav.FileSystem, context.Context) { + mux := http.NewServeMux() + fs := webdav.NewMemFS() + ctx := fillFs(t, fs) + mux.HandleFunc("/", auth(&webdav.Handler{ + FileSystem: fs, + LockSystem: webdav.NewMemLS(), + })) + srv := httptest.NewServer(mux) + return srv, fs, ctx +} + +func TestConnect(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + if err := cli.Connect(); err != nil { + t.Fatalf("got error: %v, want nil", err) + } + + cli = NewClient(srv.URL, "no", "no") + if err := cli.Connect(); err == nil { + t.Fatalf("got nil, want error: %v", err) + } +} + +func TestConnectMultipleAuth(t *testing.T) { + cli, srv, _, _ := newAuthServer(t, multipleAuth) + defer srv.Close() + if err := cli.Connect(); err != nil { + t.Fatalf("got error: %v, want nil", err) + } + + cli = NewClient(srv.URL, "digestUser", "digestPW") + if err := cli.Connect(); err != nil { + t.Fatalf("got nil, want error: %v", err) + } + + cli = NewClient(srv.URL, "no", "no") + if err := cli.Connect(); err == nil { + t.Fatalf("got nil, want error: %v", err) + } +} + +func TestConnectMultiAuthII(t *testing.T) { + cli, srv, _, _ := newAuthServer(t, func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if user, passwd, ok := r.BasicAuth(); ok { + if user == "user" && passwd == "password" { + h.ServeHTTP(w, r) + return + } + + http.Error(w, "not authorized", 403) + } else { + w.Header().Add("WWW-Authenticate", `FooAuth`) + w.Header().Add("WWW-Authenticate", `BazAuth`) + w.Header().Add("WWW-Authenticate", `BarAuth`) + w.Header().Add("WWW-Authenticate", `Basic realm="x"`) + w.WriteHeader(401) + } + } + }) + defer srv.Close() + if err := cli.Connect(); err != nil { + t.Fatalf("got error: %v, want nil", err) + } + + cli = NewClient(srv.URL, "no", "no") + if err := cli.Connect(); err == nil { + t.Fatalf("got nil, want error: %v", err) + } +} + +func TestReadDirConcurrent(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + var wg sync.WaitGroup + errs := make(chan error, 2) + for i := 0; i < 2; i++ { + wg.Add(1) + + go func() { + defer wg.Done() + f, err := cli.ReadDir("/") + if err != nil { + errs <- errors.New(fmt.Sprintf("got error: %v, want file listing: %v", err, f)) + } + if len(f) != 2 { + errs <- errors.New(fmt.Sprintf("f: %v err: %v", f, err)) + } + if f[0].Name() != "hello.txt" && f[1].Name() != "hello.txt" { + errs <- errors.New(fmt.Sprintf("got: %v, want file: %s", f, "hello.txt")) + } + if f[0].Name() != "test" && f[1].Name() != "test" { + errs <- errors.New(fmt.Sprintf("got: %v, want directory: %s", f, "test")) + } + }() + } + + wg.Wait() + close(errs) + + for err := range errs { + if err != nil { + t.Fatal(err) + } + } +} + +func TestRead(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + data, err := cli.Read("/hello.txt") + if err != nil || bytes.Compare(data, []byte("hello gowebdav\n")) != 0 { + t.Fatalf("got: %v, want data: %s", err, []byte("hello gowebdav\n")) + } + + data, err = cli.Read("/404.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", data, err) + } + if !IsErrNotFound(err) { + t.Fatalf("got: %v, want 404 error", err) + } +} + +func TestReadNoAuth(t *testing.T) { + cli, srv, _, _ := newAuthServer(t, noAuthHndl) + defer srv.Close() + + data, err := cli.Read("/hello.txt") + if err != nil || bytes.Compare(data, []byte("hello gowebdav\n")) != 0 { + t.Fatalf("got: %v, want data: %s", err, []byte("hello gowebdav\n")) + } + + data, err = cli.Read("/404.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", data, err) + } + if !IsErrNotFound(err) { + t.Fatalf("got: %v, want 404 error", err) + } +} + +func TestReadStream(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + stream, err := cli.ReadStream("/hello.txt") + if err != nil { + t.Fatalf("got: %v, want data: %v", err, stream) + } + buf := new(bytes.Buffer) + buf.ReadFrom(stream) + if buf.String() != "hello gowebdav\n" { + t.Fatalf("got: %v, want stream: hello gowebdav", buf.String()) + } + + stream, err = cli.ReadStream("/404/hello.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", stream, err) + } +} + +func TestReadStreamRange(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + stream, err := cli.ReadStreamRange("/hello.txt", 4, 4) + if err != nil { + t.Fatalf("got: %v, want data: %v", err, stream) + } + buf := new(bytes.Buffer) + buf.ReadFrom(stream) + if buf.String() != "o go" { + t.Fatalf("got: %v, want stream: o go", buf.String()) + } + + stream, err = cli.ReadStream("/404/hello.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", stream, err) + } +} + +func TestReadStreamRangeUnkownLength(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + stream, err := cli.ReadStreamRange("/hello.txt", 6, 0) + if err != nil { + t.Fatalf("got: %v, want data: %v", err, stream) + } + buf := new(bytes.Buffer) + buf.ReadFrom(stream) + if buf.String() != "gowebdav\n" { + t.Fatalf("got: %v, want stream: gowebdav\n", buf.String()) + } + + stream, err = cli.ReadStream("/404/hello.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", stream, err) + } +} + +func TestStat(t *testing.T) { + cli, srv, _, _ := newServer(t) + defer srv.Close() + + info, err := cli.Stat("/hello.txt") + if err != nil { + t.Fatalf("got: %v, want os.Info: %v", err, info) + } + if info.Name() != "hello.txt" { + t.Fatalf("got: %v, want file hello.txt", info) + } + + info, err = cli.Stat("/404.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + if !IsErrNotFound(err) { + t.Fatalf("got: %v, want 404 error", err) + } +} + +func TestMkdir(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + info, err := cli.Stat("/newdir") + if err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.Mkdir("/newdir", 0755); err != nil { + t.Fatalf("got: %v, want mkdir /newdir", err) + } + + if err := cli.Mkdir("/newdir", 0755); err != nil { + t.Fatalf("got: %v, want mkdir /newdir", err) + } + + info, err = fs.Stat(ctx, "/newdir") + if err != nil { + t.Fatalf("got: %v, want dir info: %v", err, info) + } + + if err := cli.Mkdir("/404/newdir", 0755); err == nil { + t.Fatalf("expected Mkdir error due to missing parent directory") + } +} + +func TestMkdirAll(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + if err := cli.MkdirAll("/dir/dir/dir", 0755); err != nil { + t.Fatalf("got: %v, want mkdirAll /dir/dir/dir", err) + } + + info, err := fs.Stat(ctx, "/dir/dir/dir") + if err != nil { + t.Fatalf("got: %v, want dir info: %v", err, info) + } +} + +func TestCopy(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + info, err := fs.Stat(ctx, "/copy.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.Copy("/hello.txt", "/copy.txt", false); err != nil { + t.Fatalf("got: %v, want copy /hello.txt to /copy.txt", err) + } + + info, err = fs.Stat(ctx, "/copy.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 15 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15) + } + + info, err = fs.Stat(ctx, "/hello.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 15 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15) + } + + if err := cli.Copy("/hello.txt", "/copy.txt", false); err == nil { + t.Fatalf("expected copy error due to overwrite false") + } + + if err := cli.Copy("/hello.txt", "/copy.txt", true); err != nil { + t.Fatalf("got: %v, want overwrite /copy.txt with /hello.txt", err) + } +} + +func TestRename(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + info, err := fs.Stat(ctx, "/copy.txt") + if err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.Rename("/hello.txt", "/copy.txt", false); err != nil { + t.Fatalf("got: %v, want mv /hello.txt to /copy.txt", err) + } + + info, err = fs.Stat(ctx, "/copy.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 15 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 15) + } + + if info, err = fs.Stat(ctx, "/hello.txt"); err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.Rename("/test/test.txt", "/copy.txt", true); err != nil { + t.Fatalf("got: %v, want overwrite /copy.txt with /hello.txt", err) + } + info, err = fs.Stat(ctx, "/copy.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 19 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 19) + } +} + +func TestRemove(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + if err := cli.Remove("/hello.txt"); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + if info, err := fs.Stat(ctx, "/hello.txt"); err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.Remove("/404.txt"); err != nil { + t.Fatalf("got: %v, want nil", err) + } +} + +func TestRemoveAll(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + if err := cli.RemoveAll("/test/test.txt"); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + if info, err := fs.Stat(ctx, "/test/test.txt"); err == nil { + t.Fatalf("got: %v, want error: %v", info, err) + } + + if err := cli.RemoveAll("/404.txt"); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + if err := cli.RemoveAll("/404/404/404.txt"); err != nil { + t.Fatalf("got: %v, want nil", err) + } +} + +func TestWrite(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + if err := cli.Write("/newfile.txt", []byte("foo bar\n"), 0660); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + info, err := fs.Stat(ctx, "/newfile.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 8 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8) + } + + if err := cli.Write("/404/newfile.txt", []byte("foo bar\n"), 0660); err != nil { + t.Fatalf("got: %v, want nil", err) + } +} + +func TestWriteStream(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + if err := cli.WriteStream("/newfile.txt", strings.NewReader("foo bar\n"), 0660); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + info, err := fs.Stat(ctx, "/newfile.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 8 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8) + } + + if err := cli.WriteStream("/404/works.txt", strings.NewReader("foo bar\n"), 0660); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + if info, err := fs.Stat(ctx, "/404/works.txt"); err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } +} + +func TestWriteStreamFromPipe(t *testing.T) { + cli, srv, fs, ctx := newServer(t) + defer srv.Close() + + r, w := io.Pipe() + + go func() { + defer w.Close() + fmt.Fprint(w, "foo") + time.Sleep(1 * time.Second) + fmt.Fprint(w, " ") + time.Sleep(1 * time.Second) + fmt.Fprint(w, "bar\n") + }() + + if err := cli.WriteStream("/newfile.txt", r, 0660); err != nil { + t.Fatalf("got: %v, want nil", err) + } + + info, err := fs.Stat(ctx, "/newfile.txt") + if err != nil { + t.Fatalf("got: %v, want file info: %v", err, info) + } + if info.Size() != 8 { + t.Fatalf("got: %v, want file size: %d bytes", info.Size(), 8) + } +} diff --git a/util/sync/webdavClient/digestAuth.go b/util/sync/webdavClient/digestAuth.go new file mode 100644 index 00000000..5ac63205 --- /dev/null +++ b/util/sync/webdavClient/digestAuth.go @@ -0,0 +1,164 @@ +package webdavClient + +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 { + 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 +} diff --git a/util/sync/webdavClient/digestAuth_test.go b/util/sync/webdavClient/digestAuth_test.go new file mode 100644 index 00000000..2adae4a6 --- /dev/null +++ b/util/sync/webdavClient/digestAuth_test.go @@ -0,0 +1,35 @@ +package webdavClient + +import ( + "net/http" + "strings" + "testing" +) + +func TestNewDigestAuth(t *testing.T) { + a := &DigestAuth{user: "user", pw: "password", digestParts: make(map[string]string, 0)} + + ex := "DigestAuth login: user" + if a.String() != ex { + t.Error("expected: " + ex + " got: " + a.String()) + } + + if a.Clone() == a { + t.Error("expected a different instance") + } + + if a.Close() != nil { + t.Error("expected close without errors") + } +} + +func TestDigestAuthAuthorize(t *testing.T) { + a := &DigestAuth{user: "user", pw: "password", digestParts: make(map[string]string, 0)} + rq, _ := http.NewRequest("GET", "http://localhost/", nil) + a.Authorize(nil, rq, "/") + // TODO this is a very lazy test it cuts of cnonce + ex := `Digest username="user", realm="", nonce="", uri="/", nc=1, cnonce="` + if strings.Index(rq.Header.Get("Authorization"), ex) != 0 { + t.Error("got wrong Authorization header: " + rq.Header.Get("Authorization")) + } +} diff --git a/util/sync/webdavClient/doc.go b/util/sync/webdavClient/doc.go new file mode 100644 index 00000000..db5c2455 --- /dev/null +++ b/util/sync/webdavClient/doc.go @@ -0,0 +1,3 @@ +// Package gowebdav is a WebDAV client library with a command line tool +// included. +package webdavClient diff --git a/util/sync/webdavClient/errors.go b/util/sync/webdavClient/errors.go new file mode 100644 index 00000000..13488f81 --- /dev/null +++ b/util/sync/webdavClient/errors.go @@ -0,0 +1,57 @@ +package webdavClient + +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, + } +} diff --git a/util/sync/webdavClient/file.go b/util/sync/webdavClient/file.go new file mode 100644 index 00000000..0925be67 --- /dev/null +++ b/util/sync/webdavClient/file.go @@ -0,0 +1,77 @@ +package webdavClient + +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) +} diff --git a/util/sync/webdavClient/netrc.go b/util/sync/webdavClient/netrc.go new file mode 100644 index 00000000..1bf0eaab --- /dev/null +++ b/util/sync/webdavClient/netrc.go @@ -0,0 +1,54 @@ +package webdavClient + +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 "", "" +} diff --git a/util/sync/webdavClient/passportAuth.go b/util/sync/webdavClient/passportAuth.go new file mode 100644 index 00000000..35633849 --- /dev/null +++ b/util/sync/webdavClient/passportAuth.go @@ -0,0 +1,181 @@ +package webdavClient + +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 +} diff --git a/util/sync/webdavClient/passportAuth_test.go b/util/sync/webdavClient/passportAuth_test.go new file mode 100644 index 00000000..27b8b6f0 --- /dev/null +++ b/util/sync/webdavClient/passportAuth_test.go @@ -0,0 +1,66 @@ +package webdavClient + +import ( + "bytes" + "net/http" + "net/url" + "regexp" + "testing" +) + +// testing the creation is enough as it handles the authorization during init +func TestNewPassportAuth(t *testing.T) { + user := "user" + pass := "password" + p1 := "some,comma,separated,values" + token := "from-PP='token'" + + authHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reg, err := regexp.Compile("Passport1\\.4 sign-in=" + url.QueryEscape(user) + ",pwd=" + url.QueryEscape(pass) + ",OrgVerb=GET,OrgUrl=.*," + p1) + if err != nil { + t.Error(err) + } + if reg.MatchString(r.Header.Get("Authorization")) { + w.Header().Set("Authentication-Info", token) + w.WriteHeader(200) + return + } + } + } + authsrv, _, _ := newAuthSrv(t, authHandler) + defer authsrv.Close() + + dataHandler := func(h http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reg, err := regexp.Compile("Passport1\\.4 " + token) + if err != nil { + t.Error(err) + } + if reg.MatchString(r.Header.Get("Authorization")) { + w.Header().Set("Set-Cookie", "Pass=port") + h.ServeHTTP(w, r) + return + } + for _, c := range r.Cookies() { + if c.Name == "Pass" && c.Value == "port" { + h.ServeHTTP(w, r) + return + } + } + w.Header().Set("Www-Authenticate", "Passport1.4 "+p1) + http.Redirect(w, r, authsrv.URL+"/", 302) + } + } + srv, _, _ := newAuthSrv(t, dataHandler) + defer srv.Close() + + cli := NewClient(srv.URL, user, pass) + data, err := cli.Read("/hello.txt") + if err != nil { + t.Errorf("got error=%v; want nil", err) + } + if !bytes.Equal(data, []byte("hello gowebdav\n")) { + t.Logf("got data=%v; want=hello gowebdav", data) + } +} diff --git a/util/sync/webdavClient/requests.go b/util/sync/webdavClient/requests.go new file mode 100644 index 00000000..e9c77b37 --- /dev/null +++ b/util/sync/webdavClient/requests.go @@ -0,0 +1,181 @@ +package webdavClient + +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) +} diff --git a/util/sync/webdavClient/utils.go b/util/sync/webdavClient/utils.go new file mode 100644 index 00000000..e1d4c678 --- /dev/null +++ b/util/sync/webdavClient/utils.go @@ -0,0 +1,113 @@ +package webdavClient + +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() +} diff --git a/util/sync/webdavClient/utils_test.go b/util/sync/webdavClient/utils_test.go new file mode 100644 index 00000000..74cb2506 --- /dev/null +++ b/util/sync/webdavClient/utils_test.go @@ -0,0 +1,67 @@ +package webdavClient + +import ( + "fmt" + "net/url" + "testing" +) + +func TestJoin(t *testing.T) { + eq(t, "/", "", "") + eq(t, "/", "/", "/") + eq(t, "/foo", "", "/foo") + eq(t, "foo/foo", "foo/", "/foo") + eq(t, "foo/foo", "foo/", "foo") +} + +func eq(t *testing.T, expected string, s0 string, s1 string) { + s := Join(s0, s1) + if s != expected { + t.Error("For", "'"+s0+"','"+s1+"'", "expeted", "'"+expected+"'", "got", "'"+s+"'") + } +} + +func ExamplePathEscape() { + fmt.Println(PathEscape("")) + fmt.Println(PathEscape("/")) + fmt.Println(PathEscape("/web")) + fmt.Println(PathEscape("/web/")) + fmt.Println(PathEscape("/w e b/d a v/s%u&c#k:s/")) + + // Output: + // + // / + // /web + // /web/ + // /w%20e%20b/d%20a%20v/s%25u&c%23k:s/ +} + +func TestEscapeURL(t *testing.T) { + ex := "https://foo.com/w%20e%20b/d%20a%20v/s%25u&c%23k:s/" + u, _ := url.Parse("https://foo.com" + PathEscape("/w e b/d a v/s%u&c#k:s/")) + if ex != u.String() { + t.Error("expected: " + ex + " got: " + u.String()) + } +} + +func TestFixSlashes(t *testing.T) { + expected := "/" + + if got := FixSlashes(""); got != expected { + t.Errorf("expected: %q, got: %q", expected, got) + } + + expected = "/path/" + + if got := FixSlashes("path"); got != expected { + t.Errorf("expected: %q, got: %q", expected, got) + } + + if got := FixSlashes("/path"); got != expected { + t.Errorf("expected: %q, got: %q", expected, got) + } + + if got := FixSlashes("path/"); got != expected { + t.Errorf("expected: %q, got: %q", expected, got) + } +}