Compare commits

..

4 Commits

Author SHA1 Message Date
Ringo Hoffmann
0d8627db50
Re-add setting requestBuf after seeking 2022-01-28 17:24:17 +01:00
Ringo Hoffmann
c42caf78a2
update doucmentaiton comment 2022-01-28 17:19:52 +01:00
Ringo Hoffmann
341db84788
close closable body after request 2022-01-28 17:06:32 +01:00
Ringo Hoffmann
b51247bb2c
inhibit stream close on request 2022-01-28 16:31:16 +01:00
21 changed files with 358 additions and 2049 deletions

View File

@ -15,9 +15,8 @@ jobs:
strategy: strategy:
matrix: matrix:
goversion: goversion:
- "1.20" - "1.17"
- "1.19" - "1.16"
- "1.18"
steps: steps:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v1
@ -28,4 +27,4 @@ jobs:
- name: Get dependencies - name: Get dependencies
run: go get ./... run: go get ./...
- name: Run Unit Tests - name: Run Unit Tests
run: go test -modfile=go_test.mod -v -cover -race ./... run: go test -v -cover -race ./...

10
.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: go
go:
- "1.x"
install:
- go get ./...
script:
- go test -v --short ./...

View File

@ -9,32 +9,23 @@ ${BIN}: ${SRC}
go build -o $@ ./cmd/gowebdav go build -o $@ ./cmd/gowebdav
test: test:
go test -modfile=go_test.mod -v -short -cover ./... go test -v --short ./...
api: .go/bin/godoc2md api:
@sed '/^## API$$/,$$d' -i README.md @sed '/^## API$$/,$$d' -i README.md
@echo '## API' >> README.md @echo '## API' >> README.md
@$< github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\ @godoc2md github.com/studio-b12/gowebdav | sed '/^$$/N;/^\n$$/D' |\
sed '2d' |\ sed '2d' |\
sed 's/\/src\/github.com\/studio-b12\/gowebdav\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\ 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/\/src\/target\//https:\/\/github.com\/studio-b12\/gowebdav\/blob\/master\//g' |\
sed 's/^#/##/g' >> README.md sed 's/^#/##/g' >> README.md
check: .go/bin/gocyclo check:
gofmt -w -s $(SRC) gofmt -w -s $(SRC)
@echo @echo
.go/bin/gocyclo -over 15 . gocyclo -over 15 .
@echo @echo
go vet -modfile=go_test.mod ./... golint ./...
.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: clean:
@rm -f ${BIN} @rm -f ${BIN}

437
README.md
View File

@ -1,16 +1,13 @@
# GoWebDAV # 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 Status](https://travis-ci.org/studio-b12/gowebdav.svg?branch=master)](https://travis-ci.org/studio-b12/gowebdav)
[![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) [![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) [![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). A golang WebDAV client library.
## Features at a glance
Our `gowebdav` library allows to perform following actions on the remote WebDAV server:
## Main features
`gowebdav` library allows to perform following actions on the remote WebDAV server:
* [create path](#create-path-on-a-webdav-server) * [create path](#create-path-on-a-webdav-server)
* [get files list](#get-files-list) * [get files list](#get-files-list)
* [download file](#download-file-to-byte-array) * [download file](#download-file-to-byte-array)
@ -20,17 +17,6 @@ Our `gowebdav` library allows to perform following actions on the remote WebDAV
* [copy file to another location](#copy-file-to-another-location) * [copy file to another location](#copy-file-to-another-location)
* [delete file](#delete-file) * [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 ## Usage
First of all you should create `Client` instance using `NewClient()` function: First of all you should create `Client` instance using `NewClient()` function:
@ -41,13 +27,11 @@ user := "user"
password := "password" password := "password"
c := gowebdav.NewClient(root, user, password) c := gowebdav.NewClient(root, user, password)
c.Connect()
// kick of your work!
``` ```
After you can use this `Client` to perform actions, described below. 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! **NOTICE:** we will not check errors in examples, to focus you on the `gowebdav` library's code, but you should do it in your code!
### Create path on a WebDAV server ### Create path on a WebDAV server
```go ```go
@ -73,7 +57,7 @@ webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt" localFilePath := "/tmp/webdav/file.txt"
bytes, _ := c.Read(webdavFilePath) bytes, _ := c.Read(webdavFilePath)
os.WriteFile(localFilePath, bytes, 0644) ioutil.WriteFile(localFilePath, bytes, 0644)
``` ```
### Download file via reader ### Download file via reader
@ -95,7 +79,7 @@ io.Copy(file, reader)
webdavFilePath := "folder/subfolder/file.txt" webdavFilePath := "folder/subfolder/file.txt"
localFilePath := "/tmp/webdav/file.txt" localFilePath := "/tmp/webdav/file.txt"
bytes, _ := os.ReadFile(localFilePath) bytes, _ := ioutil.ReadFile(localFilePath)
c.Write(webdavFilePath, bytes, 0644) c.Write(webdavFilePath, bytes, 0644)
``` ```
@ -175,60 +159,42 @@ Package gowebdav is a WebDAV client library with a command line tool
included. included.
### <a name="pkg-index">Index</a> ### <a name="pkg-index">Index</a>
* [Constants](#pkg-constants)
* [Variables](#pkg-variables)
* [func FixSlash(s string) string](#FixSlash) * [func FixSlash(s string) string](#FixSlash)
* [func FixSlashes(s string) string](#FixSlashes) * [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 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 PathEscape(path string) string](#PathEscape)
* [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig) * [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig)
* [func String(r io.Reader) string](#String) * [func String(r io.Reader) string](#String)
* [type AuthFactory](#AuthFactory)
* [type Authenticator](#Authenticator) * [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) * [type BasicAuth](#BasicAuth)
* [func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#BasicAuth.Authorize) * [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize)
* [func (b *BasicAuth) Clone() Authenticator](#BasicAuth.Clone) * [func (b *BasicAuth) Pass() string](#BasicAuth.Pass)
* [func (b *BasicAuth) Close() error](#BasicAuth.Close) * [func (b *BasicAuth) Type() string](#BasicAuth.Type)
* [func (b *BasicAuth) String() string](#BasicAuth.String) * [func (b *BasicAuth) User() string](#BasicAuth.User)
* [func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#BasicAuth.Verify)
* [type Client](#Client) * [type Client](#Client)
* [func NewAuthClient(uri string, auth Authorizer) *Client](#NewAuthClient)
* [func NewClient(uri, user, pw string) *Client](#NewClient) * [func NewClient(uri, user, pw string) *Client](#NewClient)
* [func (c *Client) Connect() error](#Client.Connect) * [func (c *Client) Connect() error](#Client.Connect)
* [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy) * [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) Mkdir(path string, _ os.FileMode) error](#Client.Mkdir)
* [func (c *Client) MkdirAll(path string, _ os.FileMode) (err error)](#Client.MkdirAll) * [func (c *Client) MkdirAll(path string, _ os.FileMode) error](#Client.MkdirAll)
* [func (c *Client) Read(path string) ([]byte, error)](#Client.Read) * [func (c *Client) Read(path string) ([]byte, error)](#Client.Read)
* [func (c *Client) ReadDir(path string) ([]os.FileInfo, error)](#Client.ReadDir) * [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) 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) Remove(path string) error](#Client.Remove)
* [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll) * [func (c *Client) RemoveAll(path string) error](#Client.RemoveAll)
* [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename) * [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename)
* [func (c *Client) SetHeader(key, value string)](#Client.SetHeader) * [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) 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) SetTimeout(timeout time.Duration)](#Client.SetTimeout)
* [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport) * [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport)
* [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat) * [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) Write(path string, data []byte, _ os.FileMode) error](#Client.Write)
* [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)](#Client.WriteStream) * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error](#Client.WriteStream)
* [type DigestAuth](#DigestAuth) * [type DigestAuth](#DigestAuth)
* [func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#DigestAuth.Authorize) * [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize)
* [func (d *DigestAuth) Clone() Authenticator](#DigestAuth.Clone) * [func (d *DigestAuth) Pass() string](#DigestAuth.Pass)
* [func (d *DigestAuth) Close() error](#DigestAuth.Close) * [func (d *DigestAuth) Type() string](#DigestAuth.Type)
* [func (d *DigestAuth) String() string](#DigestAuth.String) * [func (d *DigestAuth) User() string](#DigestAuth.User)
* [func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#DigestAuth.Verify)
* [type File](#File) * [type File](#File)
* [func (f File) ContentType() string](#File.ContentType) * [func (f File) ContentType() string](#File.ContentType)
* [func (f File) ETag() string](#File.ETag) * [func (f File) ETag() string](#File.ETag)
@ -240,86 +206,41 @@ included.
* [func (f File) Size() int64](#File.Size) * [func (f File) Size() int64](#File.Size)
* [func (f File) String() string](#File.String) * [func (f File) String() string](#File.String)
* [func (f File) Sys() interface{}](#File.Sys) * [func (f File) Sys() interface{}](#File.Sys)
* [type PassportAuth](#PassportAuth) * [type NoAuth](#NoAuth)
* [func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#PassportAuth.Authorize) * [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize)
* [func (p *PassportAuth) Clone() Authenticator](#PassportAuth.Clone) * [func (n *NoAuth) Pass() string](#NoAuth.Pass)
* [func (p *PassportAuth) Close() error](#PassportAuth.Close) * [func (n *NoAuth) Type() string](#NoAuth.Type)
* [func (p *PassportAuth) String() string](#PassportAuth.String) * [func (n *NoAuth) User() string](#NoAuth.User)
* [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)
##### <a name="pkg-examples">Examples</a> ##### <a name="pkg-examples">Examples</a>
* [PathEscape](#example_PathEscape) * [PathEscape](#example_PathEscape)
##### <a name="pkg-files">Package files</a> ##### <a name="pkg-files">Package files</a>
[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) [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) [file.go](https://github.com/studio-b12/gowebdav/blob/master/file.go) [netrc.go](https://github.com/studio-b12/gowebdav/blob/master/netrc.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)
### <a name="pkg-constants">Constants</a> ### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=707:737#L45)
``` go
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
```
### <a name="pkg-variables">Variables</a>
``` 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.
### <a name="FixSlash">func</a> [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=354:384#L23)
``` go ``` go
func FixSlash(s string) string func FixSlash(s string) string
``` ```
FixSlash appends a trailing / to our string FixSlash appends a trailing / to our string
### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:538#L31) ### <a name="FixSlashes">func</a> [FixSlashes](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=859:891#L53)
``` go ``` go
func FixSlashes(s string) string func FixSlashes(s string) string
``` ```
FixSlashes appends and prepends a / if they are missing FixSlashes appends and prepends a / if they are missing
### <a name="IsErrCode">func</a> [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=740:780#L29) ### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=976:1020#L61)
``` 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.
### <a name="IsErrNotFound">func</a> [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.
### <a name="Join">func</a> [Join](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=639:683#L40)
``` go ``` go
func Join(path0 string, path1 string) string func Join(path0 string, path1 string) string
``` ```
Join joins two paths Join joins two paths
### <a name="NewPathError">func</a> [NewPathError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1040:1103#L43) ### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=506:541#L36)
``` go
func NewPathError(op string, path string, statusCode int) error
```
### <a name="NewPathErrorErr">func</a> [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
```
### <a name="PathEscape">func</a> [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=153:188#L14)
``` go ``` go
func PathEscape(path string) string func PathEscape(path string) string
``` ```
PathEscape escapes all segments of a given path PathEscape escapes all segemnts of a given path
### <a name="ReadConfig">func</a> [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27) ### <a name="ReadConfig">func</a> [ReadConfig](https://github.com/studio-b12/gowebdav/blob/master/netrc.go?s=428:479#L27)
``` go ``` go
@ -328,302 +249,168 @@ func ReadConfig(uri, netrc string) (string, string)
ReadConfig reads login and password configuration from ~/.netrc ReadConfig reads login and password configuration from ~/.netrc
machine foo.com login username password 123456 machine foo.com login username password 123456
### <a name="String">func</a> [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=813:844#L45) ### <a name="String">func</a> [String](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=1150:1181#L66)
``` go ``` go
func String(r io.Reader) string func String(r io.Reader) string
``` ```
String pulls a string out of our io.Reader String pulls a string out of our io.Reader
### <a name="AuthFactory">type</a> [AuthFactory](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=150:251#L13) ### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=381:500#L28)
``` go
type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error)
```
AuthFactory prototype function to create a new Authenticator
### <a name="Authenticator">type</a> [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=2155:2695#L56)
``` go ``` go
type Authenticator interface { type Authenticator interface {
// Authorizes a request. Usually by adding some authorization headers. Type() string
Authorize(c *http.Client, rq *http.Request, path string) error User() string
// Verifies the response if the authorization was successful. Pass() string
// May trigger some round trips to pass the authentication. Authorize(*http.Request, string, string)
// 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. Authenticator stub
Each request is bound to a separate Authenticator instance.
The authentication flow itself is broken down into `Authorize` ### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9)
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.
#### <a name="NewDigestAuth">func</a> [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
#### <a name="NewPassportAuth">func</a> [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
### <a name="Authorizer">type</a> [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.
#### <a name="NewAutoAuth">func</a> [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.
#### <a name="NewEmptyAuth">func</a> [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.
#### <a name="NewPreemptiveAuth">func</a> [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.
### <a name="BasicAuth">type</a> [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L9)
``` go ``` go
type BasicAuth struct { type BasicAuth struct {
// contains filtered or unexported fields // contains filtered or unexported fields
} }
``` ```
BasicAuth structure holds our credentials BasicAuth structure holds our credentials
#### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=180:262#L15) #### <a name="BasicAuth.Authorize">func</a> (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30)
``` go ``` go
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error func (b *BasicAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
#### <a name="BasicAuth.Clone">func</a> (\*BasicAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=666:707#L34) #### <a name="BasicAuth.Pass">func</a> (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25)
``` go ``` go
func (b *BasicAuth) Clone() Authenticator func (b *BasicAuth) Pass() string
``` ```
Clone creates a Copy of itself Pass holds the BasicAuth password
#### <a name="BasicAuth.Close">func</a> (\*BasicAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=581:614#L29) #### <a name="BasicAuth.Type">func</a> (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15)
``` go ``` go
func (b *BasicAuth) Close() error func (b *BasicAuth) Type() string
``` ```
Close cleans up all resources Type identifies the BasicAuthenticator
#### <a name="BasicAuth.String">func</a> (\*BasicAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=778:813#L40) #### <a name="BasicAuth.User">func</a> (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20)
``` go ``` go
func (b *BasicAuth) String() string func (b *BasicAuth) User() string
``` ```
String toString User holds the BasicAuth username
#### <a name="BasicAuth.Verify">func</a> (\*BasicAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=352:449#L21) ### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=165:357#L17)
``` go
func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)
```
Verify verifies if the authentication
### <a name="Client">type</a> [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=220:388#L19)
``` go ``` go
type Client struct { type Client struct {
// contains filtered or unexported fields // contains filtered or unexported fields
} }
``` ```
Client defines our structure Client defines our structure
#### <a name="NewAuthClient">func</a> [NewAuthClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=608:663#L33) #### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1012:1056#L61)
``` go
func NewAuthClient(uri string, auth Authorizer) *Client
```
NewAuthClient creates a new client instance with a custom Authorizer
#### <a name="NewClient">func</a> [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=436:480#L28)
``` go ``` go
func NewClient(uri, user, pw string) *Client func NewClient(uri, user, pw string) *Client
``` ```
NewClient creates a new instance of client NewClient creates a new instance of client
#### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1829:1861#L74) #### <a name="Client.Connect">func</a> (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1836:1868#L86)
``` go ``` go
func (c *Client) Connect() error func (c *Client) Connect() error
``` ```
Connect connects to our dav server Connect connects to our dav server
#### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6815:6883#L310) #### <a name="Client.Copy">func</a> (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6695:6763#L312)
``` go ``` go
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error func (c *Client) Copy(oldpath, newpath string, overwrite bool) error
``` ```
Copy copies a file from A to B Copy copies a file from A to B
#### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5790:5852#L259) #### <a name="Client.Mkdir">func</a> (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5786:5842#L271)
``` go ``` go
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) func (c *Client) Mkdir(path string, _ os.FileMode) error
``` ```
Mkdir makes a directory Mkdir makes a directory
#### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6065:6130#L273) #### <a name="Client.MkdirAll">func</a> (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6021:6080#L282)
``` go ``` go
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) func (c *Client) MkdirAll(path string, _ os.FileMode) error
``` ```
MkdirAll like mkdir -p, but for webdav MkdirAll like mkdir -p, but for webdav
#### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6989:7039#L315) #### <a name="Client.Read">func</a> (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6869:6919#L317)
``` go ``` go
func (c *Client) Read(path string) ([]byte, error) func (c *Client) Read(path string) ([]byte, error)
``` ```
Read reads the contents of a remote file Read reads the contents of a remote file
#### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2855:2915#L117) #### <a name="Client.ReadDir">func</a> (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2862:2922#L129)
``` go ``` go
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) func (c *Client) ReadDir(path string) ([]os.FileInfo, error)
``` ```
ReadDir reads the contents of a remote directory ReadDir reads the contents of a remote directory
#### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7350:7413#L333) #### <a name="Client.ReadStream">func</a> (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7230:7293#L335)
``` go ``` go
func (c *Client) ReadStream(path string) (io.ReadCloser, error) func (c *Client) ReadStream(path string) (io.ReadCloser, error)
``` ```
ReadStream reads the stream for a given path ReadStream reads the stream for a given path
#### <a name="Client.ReadStreamRange">func</a> (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8162:8252#L355) #### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5292:5334#L248)
``` 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`.
#### <a name="Client.Remove">func</a> (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5296:5338#L236)
``` go ``` go
func (c *Client) Remove(path string) error func (c *Client) Remove(path string) error
``` ```
Remove removes a remote file Remove removes a remote file
#### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5404:5449#L241) #### <a name="Client.RemoveAll">func</a> (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5400:5445#L253)
``` go ``` go
func (c *Client) RemoveAll(path string) error func (c *Client) RemoveAll(path string) error
``` ```
RemoveAll removes remote files RemoveAll removes remote files
#### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6649:6719#L305) #### <a name="Client.Rename">func</a> (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6529:6599#L307)
``` go ``` go
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error func (c *Client) Rename(oldpath, newpath string, overwrite bool) error
``` ```
Rename moves a file from A to B Rename moves a file from A to B
#### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1092:1137#L49) #### <a name="Client.SetHeader">func</a> (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1228:1273#L66)
``` go ``` go
func (c *Client) SetHeader(key, value string) func (c *Client) SetHeader(key, value string)
``` ```
SetHeader lets us set arbitrary headers for a given client SetHeader lets us set arbitrary headers for a given client
#### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1244:1326#L54) #### <a name="Client.SetInterceptor">func</a> (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1380:1462#L71)
``` go ``` go
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))
``` ```
SetInterceptor lets us set an arbitrary interceptor for a given client SetInterceptor lets us set an arbitrary interceptor for a given client
#### <a name="Client.SetJar">func</a> (\*Client) [SetJar](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1727:1770#L69) #### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1564:1614#L76)
``` go
func (c *Client) SetJar(jar http.CookieJar)
```
SetJar exposes the ability to set a cookie jar to the client.
#### <a name="Client.SetTimeout">func</a> (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1428:1478#L59)
``` go ``` go
func (c *Client) SetTimeout(timeout time.Duration) func (c *Client) SetTimeout(timeout time.Duration)
``` ```
SetTimeout exposes the ability to set a time limit for requests SetTimeout exposes the ability to set a time limit for requests
#### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1629#L64) #### <a name="Client.SetTransport">func</a> (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1707:1765#L81)
``` go ``` go
func (c *Client) SetTransport(transport http.RoundTripper) func (c *Client) SetTransport(transport http.RoundTripper)
``` ```
SetTransport exposes the ability to define custom transports SetTransport exposes the ability to define custom transports
#### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4241:4296#L184) #### <a name="Client.Stat">func</a> (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4248:4303#L196)
``` go ``` go
func (c *Client) Stat(path string) (os.FileInfo, error) func (c *Client) Stat(path string) (os.FileInfo, error)
``` ```
Stat returns the file stats for a specified path Stat returns the file stats for a specified path
#### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9272:9347#L389) #### <a name="Client.Write">func</a> (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7584:7653#L350)
``` go ``` go
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) func (c *Client) Write(path string, data []byte, _ os.FileMode) error
``` ```
Write writes data to a given path Write writes data to a given path
#### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9771:9857#L419) #### <a name="Client.WriteStream">func</a> (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8009:8089#L373)
``` go ``` go
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error
``` ```
WriteStream writes a stream WriteStream writes a stream
@ -632,46 +419,38 @@ WriteStream writes a stream
type DigestAuth struct { type DigestAuth struct {
// contains filtered or unexported fields // contains filtered or unexported fields
} }
``` ```
DigestAuth structure holds our credentials DigestAuth structure holds our credentials
#### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=525:608#L26) #### <a name="DigestAuth.Authorize">func</a> (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36)
``` go ``` go
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error func (d *DigestAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
#### <a name="DigestAuth.Clone">func</a> (\*DigestAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1228:1270#L49) #### <a name="DigestAuth.Pass">func</a> (\*DigestAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=491:525#L31)
``` go ``` go
func (d *DigestAuth) Clone() Authenticator func (d *DigestAuth) Pass() string
``` ```
Clone creates a copy of itself Pass holds the DigestAuth password
#### <a name="DigestAuth.Close">func</a> (\*DigestAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1142:1176#L44) #### <a name="DigestAuth.Type">func</a> (\*DigestAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=299:333#L21)
``` go ``` go
func (d *DigestAuth) Close() error func (d *DigestAuth) Type() string
``` ```
Close cleans up all resources Type identifies the DigestAuthenticator
#### <a name="DigestAuth.String">func</a> (\*DigestAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1466:1502#L58) #### <a name="DigestAuth.User">func</a> (\*DigestAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=398:432#L26)
``` go ``` go
func (d *DigestAuth) String() string func (d *DigestAuth) User() string
``` ```
String toString User holds the DigestAuth username
#### <a name="DigestAuth.Verify">func</a> (\*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
### <a name="File">type</a> [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10) ### <a name="File">type</a> [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10)
``` go ``` go
type File struct { type File struct {
// contains filtered or unexported fields // contains filtered or unexported fields
} }
``` ```
File is our structure for a given file File is our structure for a given file
@ -735,59 +514,37 @@ func (f File) Sys() interface{}
``` ```
Sys ???? Sys ????
### <a name="PassportAuth">type</a> [PassportAuth](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=125:254#L12) ### <a name="NoAuth">type</a> [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=544:592#L36)
``` go ``` go
type PassportAuth struct { type NoAuth struct {
// contains filtered or unexported fields // contains filtered or unexported fields
} }
``` ```
PassportAuth structure holds our credentials NoAuth structure holds our credentials
#### <a name="PassportAuth.Authorize">func</a> (\*PassportAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=690:775#L32) #### <a name="NoAuth.Authorize">func</a> (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=887:960#L57)
``` go ``` go
func (p *PassportAuth) Authorize(c *http.Client, rq *http.Request, path string) error func (n *NoAuth) Authorize(req *http.Request, method string, path string)
``` ```
Authorize the current request Authorize the current request
#### <a name="PassportAuth.Clone">func</a> (\*PassportAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1701:1745#L69) #### <a name="NoAuth.Pass">func</a> (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=805:835#L52)
``` go ``` go
func (p *PassportAuth) Clone() Authenticator func (n *NoAuth) Pass() string
``` ```
Clone creates a Copy of itself Pass returns the current password
#### <a name="PassportAuth.Close">func</a> (\*PassportAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=1613:1649#L64) #### <a name="NoAuth.Type">func</a> (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=631:661#L42)
``` go ``` go
func (p *PassportAuth) Close() error func (n *NoAuth) Type() string
``` ```
Close cleans up all resources Type identifies the authenticator
#### <a name="PassportAuth.String">func</a> (\*PassportAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/passportAuth.go?s=2048:2086#L83) #### <a name="NoAuth.User">func</a> (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=717:747#L47)
``` go ``` go
func (p *PassportAuth) String() string func (n *NoAuth) User() string
```
String toString
#### <a name="PassportAuth.Verify">func</a> (\*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
### <a name="StatusError">type</a> [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.
#### <a name="StatusError.Error">func</a> (StatusError) [Error](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=540:576#L22)
``` go
func (se StatusError) Error() string
``` ```
User returns the current user
- - - - - -
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)

409
auth.go
View File

@ -1,409 +0,0 @@
package gowebdav
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 io.Reader = body
if body != nil {
// If the authorization fails, we will need to restart reading
// from the passed body stream.
// When body is seekable, use seek to reset the streams
// cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if _, ok := retryBuf.(io.Seeker); ok {
body = io.NopCloser(body)
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
}
a.defAuthMux.Lock()
defAuth := a.defAuth.Clone()
a.defAuthMux.Unlock()
return &authShim{factory: a.factory, body: retryBuf, auth: defAuth}, body
}
// AddAuthenticator appends the AuthFactory to our factories.
// It converts the key to lower case and preserves the order.
func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) {
key = strings.ToLower(key)
for _, f := range a.factories {
if f.key == key {
panic("Authenticator exists: " + key)
}
}
a.factories = append(a.factories, authfactory{key, fn})
}
// factory picks all valid Authenticators based on Www-Authenticate headers
func (a *authorizer) factory(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) {
headers := rs.Header.Values("Www-Authenticate")
if len(headers) > 0 {
auths := make([]Authenticator, 0)
for _, f := range a.factories {
for _, header := range headers {
headerLower := strings.ToLower(header)
if strings.Contains(headerLower, f.key) {
rs.Header.Set("Www-Authenticate", header)
if auth, err = f.create(c, rs, path); err == nil {
auths = append(auths, auth)
break
}
}
}
}
switch len(auths) {
case 0:
return nil, NewPathError("NoAuthenticator", path, rs.StatusCode)
case 1:
auth = auths[0]
default:
auth = &negoAuth{auths: auths, setDefaultAuthenticator: a.setDefaultAuthenticator}
}
} else {
auth = &noAuth{}
}
a.setDefaultAuthenticator(auth)
return auth, nil
}
// setDefaultAuthenticator sets the default Authenticator
func (a *authorizer) setDefaultAuthenticator(auth Authenticator) {
a.defAuthMux.Lock()
a.defAuth.Close()
a.defAuth = auth
a.defAuthMux.Unlock()
}
// Authorize the current request
func (s *authShim) Authorize(c *http.Client, rq *http.Request, path string) error {
if err := s.auth.Authorize(c, rq, path); err != nil {
return err
}
body := s.body
rq.GetBody = func() (io.ReadCloser, error) {
if body != nil {
if sk, ok := body.(io.Seeker); ok {
if _, err := sk.Seek(0, io.SeekStart); err != nil {
return nil, err
}
}
return io.NopCloser(body), nil
}
return nil, nil
}
return nil
}
// Verify checks for authentication issues and may trigger a re-authentication.
// Catches AlgoChangedErr to update the current Authenticator
func (s *authShim) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
redo, err = s.auth.Verify(c, rs, path)
if err != nil && errors.Is(err, ErrAuthChanged) {
if auth, aerr := s.factory(c, rs, path); aerr == nil {
s.auth.Close()
s.auth = auth
return true, nil
} else {
return false, aerr
}
}
return
}
// Close closes all resources
func (s *authShim) Close() error {
s.auth.Close()
s.auth, s.factory = nil, nil
if s.body != nil {
if closer, ok := s.body.(io.Closer); ok {
return closer.Close()
}
}
return nil
}
// It's not intend to Clone the shim
// therefore it returns a noAuth instance
func (s *authShim) Clone() Authenticator {
return &noAuth{}
}
// String toString
func (s *authShim) String() string {
return "AuthShim"
}
// Authorize authorizes the current request with the top most Authorizer
func (n *negoAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
if len(n.auths) == 0 {
return NewPathError("NoAuthenticator", path, 400)
}
return n.auths[0].Authorize(c, rq, path)
}
// Verify verifies the authentication and selects the next one based on the result
func (n *negoAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if len(n.auths) == 0 {
return false, NewPathError("NoAuthenticator", path, 400)
}
redo, err = n.auths[0].Verify(c, rs, path)
if err != nil {
if len(n.auths) > 1 {
n.auths[0].Close()
n.auths = n.auths[1:]
return true, nil
}
} else if redo {
return
} else {
auth := n.auths[0]
n.auths = n.auths[1:]
n.setDefaultAuthenticator(auth)
return
}
return false, NewPathError("NoAuthenticator", path, rs.StatusCode)
}
// Close will close the underlying authenticators.
func (n *negoAuth) Close() error {
for _, a := range n.auths {
a.Close()
}
n.setDefaultAuthenticator = nil
return nil
}
// Clone clones the underlying authenticators.
func (n *negoAuth) Clone() Authenticator {
auths := make([]Authenticator, len(n.auths))
for i, e := range n.auths {
auths[i] = e.Clone()
}
return &negoAuth{auths: auths, setDefaultAuthenticator: n.setDefaultAuthenticator}
}
func (n *negoAuth) String() string {
return "NegoAuth"
}
// Authorize the current request
func (n *noAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
return nil
}
// Verify checks for authentication issues and may trigger a re-authentication
func (n *noAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
if "" != rs.Header.Get("Www-Authenticate") {
err = ErrAuthChanged
}
return
}
// Close closes all resources
func (n *noAuth) Close() error {
return nil
}
// Clone creates a copy of itself
func (n *noAuth) Clone() Authenticator {
// no copy due to read only access
return n
}
// String toString
func (n *noAuth) String() string {
return "NoAuth"
}
// Authorize the current request
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, path string) error {
rq.Header.Set(XInhibitRedirect, "1")
return nil
}
// Verify checks for authentication issues and may trigger a re-authentication
func (n *nullAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) {
return true, ErrAuthChanged
}
// Close closes all resources
func (n *nullAuth) Close() error {
return nil
}
// Clone creates a copy of itself
func (n *nullAuth) Clone() Authenticator {
// no copy due to read only access
return n
}
// String toString
func (n *nullAuth) String() string {
return "NullAuth"
}
// NewAuthenticator creates an Authenticator (Shim) per request
func (b *preemptiveAuthorizer) NewAuthenticator(body io.Reader) (Authenticator, io.Reader) {
return b.auth.Clone(), body
}
// AddAuthenticator Will PANIC because it may only have a single authentication method
func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) {
panic("You're funny! A preemptive authorizer may only have a single authentication method")
}

View File

@ -1,62 +0,0 @@
package gowebdav
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)
}
}

View File

@ -1,7 +1,7 @@
package gowebdav package gowebdav
import ( import (
"fmt" "encoding/base64"
"net/http" "net/http"
) )
@ -11,37 +11,24 @@ type BasicAuth struct {
pw string pw string
} }
// NewDigestAuth creates a new instance of our Digest Authenticator // Type identifies the BasicAuthenticator
func NewBasicAuth(login, secret string) (Authenticator, error) { func (b *BasicAuth) Type() string {
return &BasicAuth{user: login, pw: secret}, nil return "BasicAuth"
}
// User holds the BasicAuth username
func (b *BasicAuth) User() string {
return b.user
}
// Pass holds the BasicAuth password
func (b *BasicAuth) Pass() string {
return b.pw
} }
// Authorize the current request // Authorize the current request
func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error { func (b *BasicAuth) Authorize(req *http.Request, method string, path string) {
rq.SetBasicAuth(b.user, b.pw) a := b.user + ":" + b.pw
return nil auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a))
} req.Header.Set("Authorization", auth)
// 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)
} }

View File

@ -1,51 +0,0 @@
package gowebdav
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)
}
}

157
client.go
View File

@ -10,39 +10,57 @@ import (
"os" "os"
pathpkg "path" pathpkg "path"
"strings" "strings"
"sync"
"time" "time"
) )
const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect"
// Client defines our structure // Client defines our structure
type Client struct { type Client struct {
root string root string
headers http.Header headers http.Header
interceptor func(method string, rq *http.Request) interceptor func(method string, rq *http.Request)
c *http.Client c *http.Client
auth Authorizer
authMutex sync.Mutex
auth Authenticator
}
// Authenticator stub
type Authenticator interface {
Type() string
User() string
Pass() string
Authorize(*http.Request, string, string)
}
// NoAuth structure holds our credentials
type NoAuth struct {
user string
pw string
}
// Type identifies the authenticator
func (n *NoAuth) Type() string {
return "NoAuth"
}
// User returns the current user
func (n *NoAuth) User() string {
return n.user
}
// Pass returns the current password
func (n *NoAuth) Pass() string {
return n.pw
}
// Authorize the current request
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
} }
// NewClient creates a new instance of client // NewClient creates a new instance of client
func NewClient(uri, user, pw string) *Client { func NewClient(uri, user, pw string) *Client {
return NewAuthClient(uri, NewAutoAuth(user, pw)) return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{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 // SetHeader lets us set arbitrary headers for a given client
@ -65,11 +83,6 @@ func (c *Client) SetTransport(transport http.RoundTripper) {
c.c.Transport = transport 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 // Connect connects to our dav server
func (c *Client) Connect() error { func (c *Client) Connect() error {
rs, err := c.options("/") rs, err := c.options("/")
@ -83,7 +96,7 @@ func (c *Client) Connect() error {
} }
if rs.StatusCode != 200 { if rs.StatusCode != 200 {
return NewPathError("Connect", c.root, rs.StatusCode) return newPathError("Connect", c.root, rs.StatusCode)
} }
return nil return nil
@ -127,7 +140,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
r.Props = nil r.Props = nil
return nil return nil
} }
return NewPathError("ReadDir", path, 405) return newPathError("ReadDir", path, 405)
} }
if p := getProps(r, "200"); p != nil { if p := getProps(r, "200"); p != nil {
@ -174,7 +187,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
if err != nil { if err != nil {
if _, ok := err.(*os.PathError); !ok { if _, ok := err.(*os.PathError); !ok {
err = NewPathErrorErr("ReadDir", path, err) err = newPathErrorErr("ReadDir", path, err)
} }
} }
return files, err return files, err
@ -197,7 +210,7 @@ func (c *Client) Stat(path string) (os.FileInfo, error) {
f.path += "/" f.path += "/"
} }
f.size = 0 f.size = 0
f.modified = parseModified(&p.Modified) f.modified = time.Unix(0, 0)
f.isdir = true f.isdir = true
} else { } else {
f.size = parseInt64(&p.Size) f.size = parseInt64(&p.Size)
@ -226,7 +239,7 @@ func (c *Client) Stat(path string) (os.FileInfo, error) {
if err != nil { if err != nil {
if _, ok := err.(*os.PathError); !ok { if _, ok := err.(*os.PathError); !ok {
err = NewPathErrorErr("ReadDir", path, err) err = newPathErrorErr("ReadDir", path, err)
} }
} }
return f, err return f, err
@ -241,7 +254,7 @@ func (c *Client) Remove(path string) error {
func (c *Client) RemoveAll(path string) error { func (c *Client) RemoveAll(path string) error {
rs, err := c.req("DELETE", path, nil, nil) rs, err := c.req("DELETE", path, nil, nil)
if err != nil { if err != nil {
return NewPathError("Remove", path, 400) return newPathError("Remove", path, 400)
} }
err = rs.Body.Close() err = rs.Body.Close()
if err != nil { if err != nil {
@ -252,34 +265,27 @@ func (c *Client) RemoveAll(path string) error {
return nil return nil
} }
return NewPathError("Remove", path, rs.StatusCode) return newPathError("Remove", path, rs.StatusCode)
} }
// Mkdir makes a directory // Mkdir makes a directory
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) { func (c *Client) Mkdir(path string, _ os.FileMode) error {
path = FixSlashes(path) path = FixSlashes(path)
status, err := c.mkcol(path) status := c.mkcol(path)
if err != nil {
return
}
if status == 201 { if status == 201 {
return nil return nil
} }
return NewPathError("Mkdir", path, status) return newPathError("Mkdir", path, status)
} }
// MkdirAll like mkdir -p, but for webdav // MkdirAll like mkdir -p, but for webdav
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { func (c *Client) MkdirAll(path string, _ os.FileMode) error {
path = FixSlashes(path) path = FixSlashes(path)
status, err := c.mkcol(path) status := c.mkcol(path)
if err != nil {
return
}
if status == 201 { if status == 201 {
return nil return nil
} } else if status == 409 {
if status == 409 {
paths := strings.Split(path, "/") paths := strings.Split(path, "/")
sub := "/" sub := "/"
for _, e := range paths { for _, e := range paths {
@ -287,18 +293,15 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
continue continue
} }
sub += e + "/" sub += e + "/"
status, err = c.mkcol(sub) status = c.mkcol(sub)
if err != nil {
return
}
if status != 201 { if status != 201 {
return NewPathError("MkdirAll", sub, status) return newPathError("MkdirAll", sub, status)
} }
} }
return nil return nil
} }
return NewPathError("MkdirAll", path, status) return newPathError("MkdirAll", path, status)
} }
// Rename moves a file from A to B // Rename moves a file from A to B
@ -333,7 +336,7 @@ func (c *Client) Read(path string) ([]byte, error) {
func (c *Client) ReadStream(path string) (io.ReadCloser, error) { func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, nil) rs, err := c.req("GET", path, nil, nil)
if err != nil { if err != nil {
return nil, NewPathErrorErr("ReadStream", path, err) return nil, newPathErrorErr("ReadStream", path, err)
} }
if rs.StatusCode == 200 { if rs.StatusCode == 200 {
@ -341,7 +344,7 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
} }
rs.Body.Close() rs.Body.Close()
return nil, NewPathError("ReadStream", path, rs.StatusCode) return nil, newPathError("ReadStream", path, rs.StatusCode)
} }
// ReadStreamRange reads the stream representing a subset of bytes for a given path, // ReadStreamRange reads the stream representing a subset of bytes for a given path,
@ -354,14 +357,10 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) {
// to `length`. // to `length`.
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) { func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
rs, err := c.req("GET", path, nil, func(r *http.Request) { rs, err := c.req("GET", path, nil, func(r *http.Request) {
if length > 0 { r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
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 { if err != nil {
return nil, NewPathErrorErr("ReadStreamRange", path, err) return nil, newPathErrorErr("ReadStreamRange", path, err)
} }
if rs.StatusCode == http.StatusPartialContent { if rs.StatusCode == http.StatusPartialContent {
@ -374,65 +373,55 @@ func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadClos
if rs.StatusCode == 200 { if rs.StatusCode == 200 {
// discard first 'offset' bytes. // discard first 'offset' bytes.
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
return nil, NewPathErrorErr("ReadStreamRange", path, err) return nil, newPathErrorErr("ReadStreamRange", path, err)
} }
// return a io.ReadCloser that is limited to `length` bytes. // return a io.ReadCloser that is limited to `length` bytes.
return &limitedReadCloser{rc: rs.Body, remaining: int(length)}, nil return &limitedReadCloser{rs.Body, int(length)}, nil
} }
rs.Body.Close() rs.Body.Close()
return nil, NewPathError("ReadStream", path, rs.StatusCode) return nil, newPathError("ReadStream", path, rs.StatusCode)
} }
// Write writes data to a given path // Write writes data to a given path
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) { func (c *Client) Write(path string, data []byte, _ os.FileMode) error {
s, err := c.put(path, bytes.NewReader(data)) s := c.put(path, bytes.NewReader(data))
if err != nil {
return
}
switch s { switch s {
case 200, 201, 204: case 200, 201, 204:
return nil return nil
case 404, 409: case 409:
err = c.createParentCollection(path) err := c.createParentCollection(path)
if err != nil { if err != nil {
return return err
} }
s, err = c.put(path, bytes.NewReader(data)) s = c.put(path, bytes.NewReader(data))
if err != nil {
return
}
if s == 200 || s == 201 || s == 204 { if s == 200 || s == 201 || s == 204 {
return return nil
} }
} }
return NewPathError("Write", path, s) return newPathError("Write", path, s)
} }
// WriteStream writes a stream // WriteStream writes a stream
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) { func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) error {
err = c.createParentCollection(path) err := c.createParentCollection(path)
if err != nil { if err != nil {
return err return err
} }
s, err := c.put(path, stream) s := c.put(path, stream)
if err != nil {
return err
}
switch s { switch s {
case 200, 201, 204: case 200, 201, 204:
return nil return nil
default: default:
return NewPathError("WriteStream", path, s) return newPathError("WriteStream", path, s)
} }
} }

View File

@ -1,574 +0,0 @@
package gowebdav
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)
}
}

View File

@ -4,16 +4,13 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
d "github.com/studio-b12/gowebdav"
"io" "io"
"io/fs"
"os" "os"
"os/user" "os/user"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
d "git.siteop.biz/shoopea/gowebdav"
) )
func main() { func main() {
@ -55,10 +52,6 @@ func main() {
c := d.NewClient(*root, *user, *password) c := d.NewClient(*root, *user, *password)
if e := c.Connect(); e != nil {
panic(e)
}
cmd := getCmd(*method) cmd := getCmd(*method)
if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil { if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil {
@ -197,18 +190,8 @@ func cmdCp(c *d.Client, p0, p1 string) (err error) {
func cmdPut(c *d.Client, p0, p1 string) (err error) { func cmdPut(c *d.Client, p0, p1 string) (err error) {
if p1 == "" { if p1 == "" {
p1 = path.Join(".", p0) p1 = filepath.Join(".", p0)
} else {
var fi fs.FileInfo
fi, err = c.Stat(p0)
if err != nil && !d.IsErrNotFound(err) {
return
}
if !d.IsErrNotFound(err) && fi.IsDir() {
p0 = path.Join(p0, p1)
}
} }
stream, err := getStream(p1) stream, err := getStream(p1)
if err != nil { if err != nil {
return return

View File

@ -17,46 +17,28 @@ type DigestAuth struct {
digestParts map[string]string digestParts map[string]string
} }
// NewDigestAuth creates a new instance of our Digest Authenticator // Type identifies the DigestAuthenticator
func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) { func (d *DigestAuth) Type() string {
return &DigestAuth{user: login, pw: secret, digestParts: digestParts(rs)}, nil return "DigestAuth"
}
// User holds the DigestAuth username
func (d *DigestAuth) User() string {
return d.user
}
// Pass holds the DigestAuth password
func (d *DigestAuth) Pass() string {
return d.pw
} }
// Authorize the current request // Authorize the current request
func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error { func (d *DigestAuth) Authorize(req *http.Request, method string, path string) {
d.digestParts["uri"] = path d.digestParts["uri"] = path
d.digestParts["method"] = rq.Method d.digestParts["method"] = method
d.digestParts["username"] = d.user d.digestParts["username"] = d.user
d.digestParts["password"] = d.pw d.digestParts["password"] = d.pw
rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts)) req.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 { func digestParts(resp *http.Response) map[string]string {

View File

@ -1,35 +0,0 @@
package gowebdav
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"))
}
}

View File

@ -1,57 +0,0 @@
package gowebdav
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,
}
}

2
go.mod
View File

@ -1,3 +1,3 @@
module git.siteop.biz/shoopea/gowebdav module github.com/studio-b12/gowebdav
go 1.17 go 1.17

View File

@ -1,5 +0,0 @@
module github.com/studio-b12/gowebdav
go 1.17
require golang.org/x/net v0.0.0-20221014081412-f15817d10f9b

View File

@ -1,7 +0,0 @@
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -1,181 +0,0 @@
package gowebdav
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
}

View File

@ -1,66 +0,0 @@
package gowebdav
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)
}
}

View File

@ -1,77 +1,117 @@
package gowebdav package gowebdav
import ( import (
"bytes"
"fmt"
"io" "io"
"log"
"net/http" "net/http"
"path" "path"
"strings" "strings"
) )
func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) { func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) {
var redo bool
var r *http.Request var r *http.Request
var uri = PathEscape(Join(c.root, path)) var retryBuf io.Reader
auth, body := c.auth.NewAuthenticator(body)
defer auth.Close()
for { // TODO auth.continue() strategy(true|n times|until)? if body != nil {
if r, err = http.NewRequest(method, uri, body); err != nil { // Because Request#Do closes closable streams, Seeker#Seek
return // will fail on retry because stream is already closed.
// This inhibits the closing of the passed stream on passing
// it to the RoundTripper and closes the stream after we
// are done with the body content.
if cl, ok := body.(io.Closer); ok {
body = closeInhibitor{body}
defer cl.Close()
} }
// If the authorization fails, we will need to restart reading
for k, vals := range c.headers { // from the passed body stream.
for _, v := range vals { // When body is seekable, use seek to reset the streams
r.Header.Add(k, v) // cursor to the start.
// Otherwise, copy the stream into a buffer while uploading
// and use the buffers content on retry.
if sk, ok := body.(io.Seeker); ok {
if _, err = sk.Seek(0, io.SeekStart); err != nil {
return
} }
retryBuf = body
} else {
buff := &bytes.Buffer{}
retryBuf = buff
body = io.TeeReader(body, buff)
}
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), body)
} else {
r, err = http.NewRequest(method, PathEscape(Join(c.root, path)), nil)
}
if err != nil {
return nil, err
}
for k, vals := range c.headers {
for _, v := range vals {
r.Header.Add(k, v)
}
}
// make sure we read 'c.auth' only once since it will be substituted below
// and that is unsafe to do when multiple goroutines are running at the same time.
c.authMutex.Lock()
auth := c.auth
c.authMutex.Unlock()
auth.Authorize(r, method, path)
if intercept != nil {
intercept(r)
}
if c.interceptor != nil {
c.interceptor(method, r)
}
rs, err := c.c.Do(r)
if err != nil {
return nil, err
}
if rs.StatusCode == 401 && auth.Type() == "NoAuth" {
wwwAuthenticateHeader := strings.ToLower(rs.Header.Get("Www-Authenticate"))
if strings.Index(wwwAuthenticateHeader, "digest") > -1 {
c.authMutex.Lock()
c.auth = &DigestAuth{auth.User(), auth.Pass(), digestParts(rs)}
c.authMutex.Unlock()
} else if strings.Index(wwwAuthenticateHeader, "basic") > -1 {
c.authMutex.Lock()
c.auth = &BasicAuth{auth.User(), auth.Pass()}
c.authMutex.Unlock()
} else {
return rs, newPathError("Authorize", c.root, rs.StatusCode)
} }
if err = auth.Authorize(c.c, r, path); err != nil { // retryBuf will be nil if body was nil initially so no check
return // for body == nil is required here.
} return c.req(method, path, retryBuf, intercept)
} else if rs.StatusCode == 401 {
if intercept != nil { return rs, newPathError("Authorize", c.root, rs.StatusCode)
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 return rs, err
} }
func (c *Client) mkcol(path string) (status int, err error) { func (c *Client) mkcol(path string) int {
rs, err := c.req("MKCOL", path, nil, nil) rs, err := c.req("MKCOL", path, nil, nil)
if err != nil { if err != nil {
return return 400
} }
defer rs.Body.Close() defer rs.Body.Close()
status = rs.StatusCode if rs.StatusCode == 201 || rs.StatusCode == 405 {
if status == 405 { return 201
status = 201
} }
return return rs.StatusCode
} }
func (c *Client) options(path string) (*http.Response, error) { func (c *Client) options(path string) (*http.Response, error) {
@ -99,24 +139,15 @@ func (c *Client) propfind(path string, self bool, body string, resp interface{},
defer rs.Body.Close() defer rs.Body.Close()
if rs.StatusCode != 207 { if rs.StatusCode != 207 {
return NewPathError("PROPFIND", path, rs.StatusCode) return fmt.Errorf("%s - %s %s", rs.Status, "PROPFIND", path)
} }
return parseXML(rs.Body, resp, parse) return parseXML(rs.Body, resp, parse)
} }
func (c *Client) doCopyMove( func (c *Client) doCopyMove(method string, oldpath string, newpath string, overwrite bool) (int, io.ReadCloser) {
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) { rs, err := c.req(method, oldpath, nil, func(rq *http.Request) {
rq.Header.Add("Destination", PathEscape(Join(c.root, newpath))) rq.Header.Add("Destination", Join(c.root, newpath))
if overwrite { if overwrite {
rq.Header.Add("Overwrite", "T") rq.Header.Add("Overwrite", "T")
} else { } else {
@ -124,18 +155,13 @@ func (c *Client) doCopyMove(
} }
}) })
if err != nil { if err != nil {
return return 400, nil
} }
status = rs.StatusCode return rs.StatusCode, rs.Body
r = rs.Body
return
} }
func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) (err error) { func (c *Client) copymove(method string, oldpath string, newpath string, overwrite bool) error {
s, data, err := c.doCopyMove(method, oldpath, newpath, overwrite) s, data := c.doCopyMove(method, oldpath, newpath, overwrite)
if err != nil {
return
}
if data != nil { if data != nil {
defer data.Close() defer data.Close()
} }
@ -146,7 +172,7 @@ func (c *Client) copymove(method string, oldpath string, newpath string, overwri
case 207: case 207:
// TODO handle multistat errors, worst case ... // TODO handle multistat errors, worst case ...
log.Printf("TODO handle %s - %s multistatus result %s\n", method, oldpath, String(data)) log(fmt.Sprintf(" TODO handle %s - %s multistatus result %s", method, oldpath, String(data)))
case 409: case 409:
err := c.createParentCollection(newpath) err := c.createParentCollection(newpath)
@ -157,18 +183,17 @@ func (c *Client) copymove(method string, oldpath string, newpath string, overwri
return c.copymove(method, oldpath, newpath, overwrite) return c.copymove(method, oldpath, newpath, overwrite)
} }
return NewPathError(method, oldpath, s) return newPathError(method, oldpath, s)
} }
func (c *Client) put(path string, stream io.Reader) (status int, err error) { func (c *Client) put(path string, stream io.Reader) int {
rs, err := c.req("PUT", path, stream, nil) rs, err := c.req("PUT", path, stream, nil)
if err != nil { if err != nil {
return return 400
} }
defer rs.Body.Close() defer rs.Body.Close()
status = rs.StatusCode return rs.StatusCode
return
} }
func (c *Client) createParentCollection(itemPath string) (err error) { func (c *Client) createParentCollection(itemPath string) (err error) {

View File

@ -3,13 +3,35 @@ package gowebdav
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
func log(msg interface{}) {
fmt.Println(msg)
}
func newPathError(op string, path string, statusCode int) error {
return &os.PathError{
Op: op,
Path: path,
Err: fmt.Errorf("%d", statusCode),
}
}
func newPathErrorErr(op string, path string, err error) error {
return &os.PathError{
Op: op,
Path: path,
Err: err,
}
}
// PathEscape escapes all segments of a given path // PathEscape escapes all segments of a given path
func PathEscape(path string) string { func PathEscape(path string) string {
s := strings.Split(path, "/") s := strings.Split(path, "/")
@ -111,3 +133,14 @@ func (l *limitedReadCloser) Read(buf []byte) (int, error) {
func (l *limitedReadCloser) Close() error { func (l *limitedReadCloser) Close() error {
return l.rc.Close() return l.rc.Close()
} }
// closeInhibitor implements io.Closer and
// wraps a Reader. When Close() is performed
// on it, it will simply be silently rejected.
type closeInhibitor struct {
io.Reader
}
func (ci closeInhibitor) Close() error {
return nil
}