From ca40e2802e7ca5c02852e956e62e1e7942b1661f Mon Sep 17 00:00:00 2001 From: Christoph Polcin Date: Fri, 3 Feb 2023 10:18:35 +0100 Subject: [PATCH] Feat: Authentication API The changes simplify the `req` method by moving the authentication-related code into the API. This makes it easy to add additional authentication methods. The API introduces an `Authorizer` that acts as an authenticator factory. The authentication flow itself is divided down into `Authorize` and `Verify` steps in order to encapsulate and control complex authentication challenges. The default `NewAutoAuth` negotiates the algorithms. Under the hood, it creates an authenticator shim per request, which delegates the authentication flow to our authenticators. The `NewEmptyAuth` and `NewPreemptiveAuth` authorizers allow you to have more control over algorithms and resources. The API also allows interception of the redirect mechanism by setting the `XInhibitRedirect` header. This closes: #15 #24 #38 --- README.md | 336 +++++++++++++++++++++++++------------ auth.go | 388 +++++++++++++++++++++++++++++++++++++++++++ auth_test.go | 62 +++++++ basicAuth.go | 48 +++--- basicAuth_test.go | 51 ++++++ client.go | 90 ++++------ client_test.go | 79 ++++++++- cmd/gowebdav/main.go | 4 + digestAuth.go | 54 +++--- digestAuth_test.go | 35 ++++ errors.go | 12 +- requests.go | 120 +++++-------- 12 files changed, 994 insertions(+), 285 deletions(-) create mode 100644 auth.go create mode 100644 auth_test.go create mode 100644 basicAuth_test.go create mode 100644 digestAuth_test.go diff --git a/README.md b/README.md index 50fc0d8..0191173 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ [![GoDoc](https://godoc.org/github.com/studio-b12/gowebdav?status.svg)](https://godoc.org/github.com/studio-b12/gowebdav) [![Go Report Card](https://goreportcard.com/badge/github.com/studio-b12/gowebdav)](https://goreportcard.com/report/github.com/studio-b12/gowebdav) -A golang WebDAV client library. +A pure Golang WebDAV client library that comes with a reference implementation. -## Main features +## Features at a glance + +Our `gowebdav` library allows to perform following actions on the remote WebDAV server: -`gowebdav` library allows to perform following actions on the remote WebDAV server: * [create path](#create-path-on-a-webdav-server) * [get files list](#get-files-list) * [download file](#download-file-to-byte-array) @@ -19,6 +20,16 @@ A golang WebDAV client library. * [copy file to another location](#copy-file-to-another-location) * [delete file](#delete-file) +It also provides an [authentication API](#type-authenticator) that makes it easy to encapsulate and control complex authentication challenges. +The default implementation negotiates the algorithm based on the user's preferences and the methods offered by the remote server. + +Out-of-box support for: + +* [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication) +* [DigestAuth](https://en.wikipedia.org/wiki/Digest_access_authentication) +* [Kerberos comming](https://github.com/studio-b12/gowebdav/pull/71#issuecomment-1416465334) + + ## Usage First of all you should create `Client` instance using `NewClient()` function: @@ -29,11 +40,13 @@ user := "user" password := "password" c := gowebdav.NewClient(root, user, password) +c.Connect() +// kick of your work! ``` After you can use this `Client` to perform actions, described below. -**NOTICE:** we will not check errors in examples, to focus you on the `gowebdav` library's code, but you should do it in your code! +**NOTICE:** We will not check for errors in the examples, to focus you on the `gowebdav` library's code, but you should do it in your code! ### Create path on a WebDAV server ```go @@ -59,7 +72,7 @@ webdavFilePath := "folder/subfolder/file.txt" localFilePath := "/tmp/webdav/file.txt" bytes, _ := c.Read(webdavFilePath) -ioutil.WriteFile(localFilePath, bytes, 0644) +os.WriteFile(localFilePath, bytes, 0644) ``` ### Download file via reader @@ -81,7 +94,7 @@ io.Copy(file, reader) webdavFilePath := "folder/subfolder/file.txt" localFilePath := "/tmp/webdav/file.txt" -bytes, _ := ioutil.ReadFile(localFilePath) +bytes, _ := os.ReadFile(localFilePath) c.Write(webdavFilePath, bytes, 0644) ``` @@ -161,21 +174,33 @@ Package gowebdav is a WebDAV client library with a command line tool included. ### Index +* [Constants](#pkg-constants) +* [Variables](#pkg-variables) * [func FixSlash(s string) string](#FixSlash) * [func FixSlashes(s string) string](#FixSlashes) * [func IsErrCode(err error, code int) bool](#IsErrCode) * [func IsErrNotFound(err error) bool](#IsErrNotFound) * [func Join(path0 string, path1 string) string](#Join) +* [func NewPathError(op string, path string, statusCode int) error](#NewPathError) +* [func NewPathErrorErr(op string, path string, err error) error](#NewPathErrorErr) * [func PathEscape(path string) string](#PathEscape) * [func ReadConfig(uri, netrc string) (string, string)](#ReadConfig) * [func String(r io.Reader) string](#String) +* [type AuthFactory](#AuthFactory) * [type Authenticator](#Authenticator) + * [func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error)](#NewDigestAuth) +* [type Authorizer](#Authorizer) + * [func NewAutoAuth(login string, secret string) Authorizer](#NewAutoAuth) + * [func NewEmptyAuth() Authorizer](#NewEmptyAuth) + * [func NewPreemptiveAuth(auth Authenticator) Authorizer](#NewPreemptiveAuth) * [type BasicAuth](#BasicAuth) - * [func (b *BasicAuth) Authorize(req *http.Request, method string, path string)](#BasicAuth.Authorize) - * [func (b *BasicAuth) Pass() string](#BasicAuth.Pass) - * [func (b *BasicAuth) Type() string](#BasicAuth.Type) - * [func (b *BasicAuth) User() string](#BasicAuth.User) + * [func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#BasicAuth.Authorize) + * [func (b *BasicAuth) Clone() Authenticator](#BasicAuth.Clone) + * [func (b *BasicAuth) Close() error](#BasicAuth.Close) + * [func (b *BasicAuth) String() string](#BasicAuth.String) + * [func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#BasicAuth.Verify) * [type Client](#Client) + * [func NewAuthClient(uri string, auth Authorizer) *Client](#NewAuthClient) * [func NewClient(uri, user, pw string) *Client](#NewClient) * [func (c *Client) Connect() error](#Client.Connect) * [func (c *Client) Copy(oldpath, newpath string, overwrite bool) error](#Client.Copy) @@ -190,16 +215,18 @@ included. * [func (c *Client) Rename(oldpath, newpath string, overwrite bool) error](#Client.Rename) * [func (c *Client) SetHeader(key, value string)](#Client.SetHeader) * [func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request))](#Client.SetInterceptor) + * [func (c *Client) SetJar(jar http.CookieJar)](#Client.SetJar) * [func (c *Client) SetTimeout(timeout time.Duration)](#Client.SetTimeout) * [func (c *Client) SetTransport(transport http.RoundTripper)](#Client.SetTransport) * [func (c *Client) Stat(path string) (os.FileInfo, error)](#Client.Stat) * [func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error)](#Client.Write) * [func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error)](#Client.WriteStream) * [type DigestAuth](#DigestAuth) - * [func (d *DigestAuth) Authorize(req *http.Request, method string, path string)](#DigestAuth.Authorize) - * [func (d *DigestAuth) Pass() string](#DigestAuth.Pass) - * [func (d *DigestAuth) Type() string](#DigestAuth.Type) - * [func (d *DigestAuth) User() string](#DigestAuth.User) + * [func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error](#DigestAuth.Authorize) + * [func (d *DigestAuth) Clone() Authenticator](#DigestAuth.Clone) + * [func (d *DigestAuth) Close() error](#DigestAuth.Close) + * [func (d *DigestAuth) String() string](#DigestAuth.String) + * [func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error)](#DigestAuth.Verify) * [type File](#File) * [func (f File) ContentType() string](#File.ContentType) * [func (f File) ETag() string](#File.ETag) @@ -211,11 +238,6 @@ included. * [func (f File) Size() int64](#File.Size) * [func (f File) String() string](#File.String) * [func (f File) Sys() interface{}](#File.Sys) -* [type NoAuth](#NoAuth) - * [func (n *NoAuth) Authorize(req *http.Request, method string, path string)](#NoAuth.Authorize) - * [func (n *NoAuth) Pass() string](#NoAuth.Pass) - * [func (n *NoAuth) Type() string](#NoAuth.Type) - * [func (n *NoAuth) User() string](#NoAuth.User) * [type StatusError](#StatusError) * [func (se StatusError) Error() string](#StatusError.Error) @@ -223,7 +245,24 @@ included. * [PathEscape](#example_PathEscape) ##### Package files -[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) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go) +[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) [requests.go](https://github.com/studio-b12/gowebdav/blob/master/requests.go) [utils.go](https://github.com/studio-b12/gowebdav/blob/master/utils.go) + +### Constants +``` go +const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" +``` + +### Variables +``` go +var ErrAuthChanged = errors.New("authentication failed, change algorithm") +``` +ErrAuthChanged must be returned from the Verify method as an error +to trigger a re-authentication / negotiation with a new authenticator. + +``` go +var ErrTooManyRedirects = errors.New("stopped after 10 redirects") +``` +ErrTooManyRedirects will be used as return error if a request exceeds 10 redirects. ### func [FixSlash](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=354:384#L23) ``` go @@ -237,7 +276,7 @@ func FixSlashes(s string) string ``` FixSlashes appends and prepends a / if they are missing -### func [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=355:395#L21) +### func [IsErrCode](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=740:780#L29) ``` go func IsErrCode(err error, code int) bool ``` @@ -245,7 +284,7 @@ IsErrCode returns true if the given error is an os.PathError wrapping a StatusError with the given status code. -### func [IsErrNotFound](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=587:621#L31) +### func [IsErrNotFound](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=972:1006#L39) ``` go func IsErrNotFound(err error) bool ``` @@ -258,6 +297,16 @@ func Join(path0 string, path1 string) string ``` Join joins two paths +### func [NewPathError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1040:1103#L43) +``` go +func NewPathError(op string, path string, statusCode int) error +``` + +### func [NewPathErrorErr](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=1194:1255#L51) +``` go +func NewPathErrorErr(op string, path string, err error) error +``` + ### func [PathEscape](https://github.com/studio-b12/gowebdav/blob/master/utils.go?s=153:188#L14) ``` go func PathEscape(path string) string @@ -277,18 +326,102 @@ func String(r io.Reader) string ``` String pulls a string out of our io.Reader -### type [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=388:507#L29) +### type [AuthFactory](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=150:251#L13) +``` go +type AuthFactory func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) +``` +AuthFactory prototype function to create a new Authenticator + +### type [Authenticator](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=1875:2084#L51) ``` go type Authenticator interface { - Type() string - User() string - Pass() string - Authorize(*http.Request, string, string) + Authorize(c *http.Client, rq *http.Request, path string) error + Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) + Clone() Authenticator + io.Closer } ``` -Authenticator stub +A Authenticator implements a specific way to authorize requests. +Each request is bound to a separate Authenticator instance. -### type [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=106:157#L9) +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 roundtrips, +which can be archived by returning the `redo` of the Verify +method. `True` restarts the authentication process for the +current action: A new `Request` is spawned, which must be +authorized, sent, and re-verified again, until the action +is successfully submitted. +The preferred way is to handle the authentication ping-pong +within `Verify`, and then `redo` with fresh credentials. + +The result of the `Verify` method can also trigger an +`Authenticator` change by returning the `ErrAuthChanged` +as an error. Depending on the `Authorizer` this may trigger +an `Authenticator` negotiation. + +Set the `XInhibitRedirect` header to '1' in the `Authorize` +method to get control over request redirection. +Attention! You must handle the incoming request yourself. + +To store a shared session state the `Clone` method **must** +return a new instance, initialized with the shared state. + +#### func [NewDigestAuth](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=324:406#L21) +``` go +func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) +``` +NewDigestAuth creates a new instance of our Digest Authenticator + +### type [Authorizer](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=349:485#L17) +``` go +type Authorizer interface { + NewAuthenticator(body io.Reader) (Authenticator, io.Reader) + AddAuthenticator(key string, fn AuthFactory) +} +``` +Authorizer our Authenticator factory which creates an +`Authenticator` per action/request. + +#### func [NewAutoAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=3178:3234#L99) +``` go +func NewAutoAuth(login string, secret string) Authorizer +``` +NewAutoAuth creates an auto Authenticator factory. +It negotiates the default authentication method +based on the order of the registered Authenticators +and the remotely offered authentication methods. +First In, First Out. + +#### func [NewEmptyAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=3833:3863#L118) +``` go +func NewEmptyAuth() Authorizer +``` +NewEmptyAuth creates an empty Authenticator factory +The order of adding the Authenticator matters. +First In, First Out. +It offers the `NewAutoAuth` features. + +#### func [NewPreemptiveAuth](https://github.com/studio-b12/gowebdav/blob/master/auth.go?s=4403:4456#L134) +``` go +func NewPreemptiveAuth(auth Authenticator) Authorizer +``` +NewPreemptiveAuth creates a preemptive Authenticator +The preemptive authorizer uses the provided Authenticator +for every request regardless of any `Www-Authenticate` header. + +It may only have one authentication method, +so calling `AddAuthenticator` will panic! + +Look out!! This offers the skinniest and slickest implementation +without any synchronisation!! +Still applicable with `BasicAuth` within go routines. + +### type [BasicAuth](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=94:145#L9) ``` go type BasicAuth struct { // contains filtered or unexported fields @@ -297,31 +430,37 @@ type BasicAuth struct { ``` BasicAuth structure holds our credentials -#### func (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=473:549#L30) +#### func (\*BasicAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=180:262#L15) ``` go -func (b *BasicAuth) Authorize(req *http.Request, method string, path string) +func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error ``` Authorize the current request -#### func (\*BasicAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=388:421#L25) +#### func (\*BasicAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=666:707#L34) ``` go -func (b *BasicAuth) Pass() string +func (b *BasicAuth) Clone() Authenticator ``` -Pass holds the BasicAuth password +Clone creates a Copy of itself -#### func (\*BasicAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=201:234#L15) +#### func (\*BasicAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=581:614#L29) ``` go -func (b *BasicAuth) Type() string +func (b *BasicAuth) Close() error ``` -Type identifies the BasicAuthenticator +Close cleans up all resources -#### func (\*BasicAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=297:330#L20) +#### func (\*BasicAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=778:813#L40) ``` go -func (b *BasicAuth) User() string +func (b *BasicAuth) String() string ``` -User holds the BasicAuth username +String toString -### type [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=172:364#L18) +#### func (\*BasicAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/basicAuth.go?s=352:449#L21) +``` go +func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) +``` +Verify verifies if the authentication + +### type [Client](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=220:388#L19) ``` go type Client struct { // contains filtered or unexported fields @@ -330,55 +469,61 @@ type Client struct { ``` Client defines our structure -#### func [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1019:1063#L62) +#### func [NewAuthClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=608:663#L33) +``` go +func NewAuthClient(uri string, auth Authorizer) *Client +``` +NewAuthClient creates a new client instance with a custom Authorizer + +#### func [NewClient](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=436:480#L28) ``` go func NewClient(uri, user, pw string) *Client ``` NewClient creates a new instance of client -#### func (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1843:1875#L87) +#### func (\*Client) [Connect](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1792:1824#L74) ``` go func (c *Client) Connect() error ``` Connect connects to our dav server -#### func (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6818:6886#L323) +#### func (\*Client) [Copy](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6767:6835#L310) ``` go func (c *Client) Copy(oldpath, newpath string, overwrite bool) error ``` Copy copies a file from A to B -#### func (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5793:5855#L272) +#### func (\*Client) [Mkdir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5742:5804#L259) ``` go func (c *Client) Mkdir(path string, _ os.FileMode) (err error) ``` Mkdir makes a directory -#### func (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6068:6133#L286) +#### func (\*Client) [MkdirAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6017:6082#L273) ``` go func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) ``` MkdirAll like mkdir -p, but for webdav -#### func (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6992:7042#L328) +#### func (\*Client) [Read](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6941:6991#L315) ``` go func (c *Client) Read(path string) ([]byte, error) ``` Read reads the contents of a remote file -#### func (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2869:2929#L130) +#### func (\*Client) [ReadDir](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=2818:2878#L117) ``` go func (c *Client) ReadDir(path string) ([]os.FileInfo, error) ``` ReadDir reads the contents of a remote directory -#### func (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7353:7416#L346) +#### func (\*Client) [ReadStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=7302:7365#L333) ``` go func (c *Client) ReadStream(path string) (io.ReadCloser, error) ``` ReadStream reads the stream for a given path -#### func (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8165:8255#L368) +#### func (\*Client) [ReadStreamRange](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=8114:8204#L355) ``` go func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) ``` @@ -391,61 +536,67 @@ If the server does not support partial content requests and returns full content this function will emulate the behavior by skipping `offset` bytes and limiting the result to `length`. -#### func (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5299:5341#L249) +#### func (\*Client) [Remove](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5248:5290#L236) ``` go func (c *Client) Remove(path string) error ``` Remove removes a remote file -#### func (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5407:5452#L254) +#### func (\*Client) [RemoveAll](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=5356:5401#L241) ``` go func (c *Client) RemoveAll(path string) error ``` RemoveAll removes remote files -#### func (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6652:6722#L318) +#### func (\*Client) [Rename](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=6601:6671#L305) ``` go func (c *Client) Rename(oldpath, newpath string, overwrite bool) error ``` Rename moves a file from A to B -#### func (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1235:1280#L67) +#### func (\*Client) [SetHeader](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1055:1100#L49) ``` go func (c *Client) SetHeader(key, value string) ``` SetHeader lets us set arbitrary headers for a given client -#### func (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1387:1469#L72) +#### func (\*Client) [SetInterceptor](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1207:1289#L54) ``` go func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) ``` SetInterceptor lets us set an arbitrary interceptor for a given client -#### func (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1571:1621#L77) +#### func (\*Client) [SetJar](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1690:1733#L69) +``` go +func (c *Client) SetJar(jar http.CookieJar) +``` +SetJar exposes the ability to set a cookie jar to the client. + +#### func (\*Client) [SetTimeout](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1391:1441#L59) ``` go func (c *Client) SetTimeout(timeout time.Duration) ``` SetTimeout exposes the ability to set a time limit for requests -#### func (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1714:1772#L82) +#### func (\*Client) [SetTransport](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=1534:1592#L64) ``` go func (c *Client) SetTransport(transport http.RoundTripper) ``` SetTransport exposes the ability to define custom transports -#### func (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4255:4310#L197) +#### func (\*Client) [Stat](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=4204:4259#L184) ``` go func (c *Client) Stat(path string) (os.FileInfo, error) ``` Stat returns the file stats for a specified path -#### func (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9260:9335#L402) +#### func (\*Client) [Write](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9209:9284#L389) ``` go func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) ``` Write writes data to a given path -#### func (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9759:9845#L432) +#### func (\*Client) [WriteStream](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=9708:9794#L419) ``` go func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err error) ``` @@ -460,29 +611,35 @@ type DigestAuth struct { ``` DigestAuth structure holds our credentials -#### func (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=577:654#L36) +#### func (\*DigestAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=502:585#L26) ``` go -func (d *DigestAuth) Authorize(req *http.Request, method string, path string) +func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error ``` Authorize the current request -#### func (\*DigestAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=491:525#L31) +#### func (\*DigestAuth) [Clone](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1205:1247#L49) ``` go -func (d *DigestAuth) Pass() string +func (d *DigestAuth) Clone() Authenticator ``` -Pass holds the DigestAuth password +Clone creates a copy of itself -#### func (\*DigestAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=299:333#L21) +#### func (\*DigestAuth) [Close](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1119:1153#L44) ``` go -func (d *DigestAuth) Type() string +func (d *DigestAuth) Close() error ``` -Type identifies the DigestAuthenticator +Close cleans up all resources -#### func (\*DigestAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=398:432#L26) +#### func (\*DigestAuth) [String](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=1420:1456#L58) ``` go -func (d *DigestAuth) User() string +func (d *DigestAuth) String() string ``` -User holds the DigestAuth username +String toString + +#### func (\*DigestAuth) [Verify](https://github.com/studio-b12/gowebdav/blob/master/digestAuth.go?s=889:987#L36) +``` go +func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) +``` +Verify checks for authentication issues and may trigger a re-authentication ### type [File](https://github.com/studio-b12/gowebdav/blob/master/file.go?s=93:253#L10) ``` go @@ -553,40 +710,7 @@ func (f File) Sys() interface{} ``` Sys ???? -### type [NoAuth](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=551:599#L37) -``` go -type NoAuth struct { - // contains filtered or unexported fields -} - -``` -NoAuth structure holds our credentials - -#### func (\*NoAuth) [Authorize](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=894:967#L58) -``` go -func (n *NoAuth) Authorize(req *http.Request, method string, path string) -``` -Authorize the current request - -#### func (\*NoAuth) [Pass](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=812:842#L53) -``` go -func (n *NoAuth) Pass() string -``` -Pass returns the current password - -#### func (\*NoAuth) [Type](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=638:668#L43) -``` go -func (n *NoAuth) Type() string -``` -Type identifies the authenticator - -#### func (\*NoAuth) [User](https://github.com/studio-b12/gowebdav/blob/master/client.go?s=724:754#L48) -``` go -func (n *NoAuth) User() string -``` -User returns the current user - -### type [StatusError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=114:153#L10) +### type [StatusError](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=499:538#L18) ``` go type StatusError struct { Status int @@ -596,7 +720,7 @@ type StatusError struct { StatusError implements error and wraps an erroneous status code. -#### func (StatusError) [Error](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=155:191#L14) +#### func (StatusError) [Error](https://github.com/studio-b12/gowebdav/blob/master/errors.go?s=540:576#L22) ``` go func (se StatusError) Error() string ``` diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..3584360 --- /dev/null +++ b/auth.go @@ -0,0 +1,388 @@ +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 { + NewAuthenticator(body io.Reader) (Authenticator, io.Reader) + 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 roundtrips, +// 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 { + Authorize(c *http.Client, rq *http.Request, path string) error + Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) + Clone() Authenticator + io.Closer +} + +type authfactory struct { + key string + create AuthFactory +} + +// 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{fmap, sync.Mutex{}, &nullAuth{}} + + az.AddAuthenticator("basic", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return &BasicAuth{login, secret}, nil + }) + + az.AddAuthenticator("digest", func(c *http.Client, rs *http.Response, path string) (auth Authenticator, err error) { + return NewDigestAuth(login, secret, rs) + }) + + 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{fmap, sync.Mutex{}, &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} +} + +// 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{a.factory, retryBuf, 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, 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 +} + +// Do not Clone the shim, it ends badly. In any case for you. +func (s *authShim) Clone() Authenticator { + panic("Do not Clone the shim, it ends badly. In any case for you.") +} + +// 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 { + 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) { + 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, 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 A preemptive authorizer may only have a single authentication method +func (b *preemptiveAuthorizer) AddAuthenticator(key string, fn AuthFactory) { + panic("You're funny! A preemptive authorizer may only have a single authentication method") +} diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..868d7c4 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,62 @@ +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) + } +} diff --git a/basicAuth.go b/basicAuth.go index bdb86da..5511a60 100644 --- a/basicAuth.go +++ b/basicAuth.go @@ -1,7 +1,7 @@ package gowebdav import ( - "encoding/base64" + "fmt" "net/http" ) @@ -11,24 +11,32 @@ type BasicAuth struct { pw string } -// Type identifies the BasicAuthenticator -func (b *BasicAuth) Type() string { - 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 -func (b *BasicAuth) Authorize(req *http.Request, method string, path string) { - a := b.user + ":" + b.pw - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a)) - req.Header.Set("Authorization", auth) +func (b *BasicAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + rq.SetBasicAuth(b.user, b.pw) + return nil +} + +// Verify verifies if the authentication +func (b *BasicAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if rs.StatusCode == 401 { + err = NewPathError("Authorize", path, rs.StatusCode) + } + return +} + +// Close cleans up all resources +func (b *BasicAuth) Close() error { + return nil +} + +// Clone creates a Copy of itself +func (b *BasicAuth) Clone() Authenticator { + // no copy due to read only access + return b +} + +// String toString +func (b *BasicAuth) String() string { + return fmt.Sprintf("BasicAuth login: %s", b.user) } diff --git a/basicAuth_test.go b/basicAuth_test.go new file mode 100644 index 0000000..a899e2d --- /dev/null +++ b/basicAuth_test.go @@ -0,0 +1,51 @@ +package gowebdav + +import ( + "net/http" + "testing" +) + +func TestNewBasicAuth(t *testing.T) { + a := &BasicAuth{"user", "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", "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", "password"} + auth := NewPreemptiveAuth(a) + n, b := auth.NewAuthenticator(nil) + if b != nil { + t.Error("expected body to be nil") + } + if n != a { + t.Error("expected the same instance") + } + + srv, _, _ := newAuthSrv(t, basicAuth) + defer srv.Close() + cli := NewAuthClient(srv.URL, auth) + if err := cli.Connect(); err != nil { + t.Fatalf("got error: %v, want nil", err) + } +} diff --git a/client.go b/client.go index df68846..6c044dc 100644 --- a/client.go +++ b/client.go @@ -10,57 +10,39 @@ import ( "os" pathpkg "path" "strings" - "sync" "time" ) +const XInhibitRedirect = "X-Gowebdav-Inhibit-Redirect" + // Client defines our structure type Client struct { root string headers http.Header interceptor func(method string, rq *http.Request) c *http.Client - - 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) { + auth Authorizer } // NewClient creates a new instance of client func NewClient(uri, user, pw string) *Client { - return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}} + return NewAuthClient(uri, NewAutoAuth(user, pw)) +} + +// NewAuthClient creates a new client instance with a custom Authorizer +func NewAuthClient(uri string, auth Authorizer) *Client { + c := &http.Client{ + CheckRedirect: func(rq *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return ErrTooManyRedirects + } + if via[0].Header.Get(XInhibitRedirect) != "" { + return http.ErrUseLastResponse + } + return nil + }, + } + return &Client{FixSlash(uri), make(http.Header), nil, c, auth} } // SetHeader lets us set arbitrary headers for a given client @@ -101,7 +83,7 @@ func (c *Client) Connect() error { } if rs.StatusCode != 200 { - return newPathError("Connect", c.root, rs.StatusCode) + return NewPathError("Connect", c.root, rs.StatusCode) } return nil @@ -145,7 +127,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { r.Props = nil return nil } - return newPathError("ReadDir", path, 405) + return NewPathError("ReadDir", path, 405) } if p := getProps(r, "200"); p != nil { @@ -192,7 +174,7 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { if err != nil { if _, ok := err.(*os.PathError); !ok { - err = newPathErrorErr("ReadDir", path, err) + err = NewPathErrorErr("ReadDir", path, err) } } return files, err @@ -244,7 +226,7 @@ func (c *Client) Stat(path string) (os.FileInfo, error) { if err != nil { if _, ok := err.(*os.PathError); !ok { - err = newPathErrorErr("ReadDir", path, err) + err = NewPathErrorErr("ReadDir", path, err) } } return f, err @@ -259,7 +241,7 @@ func (c *Client) Remove(path string) error { func (c *Client) RemoveAll(path string) error { rs, err := c.req("DELETE", path, nil, nil) if err != nil { - return newPathError("Remove", path, 400) + return NewPathError("Remove", path, 400) } err = rs.Body.Close() if err != nil { @@ -270,7 +252,7 @@ func (c *Client) RemoveAll(path string) error { return nil } - return newPathError("Remove", path, rs.StatusCode) + return NewPathError("Remove", path, rs.StatusCode) } // Mkdir makes a directory @@ -284,7 +266,7 @@ func (c *Client) Mkdir(path string, _ os.FileMode) (err error) { return nil } - return newPathError("Mkdir", path, status) + return NewPathError("Mkdir", path, status) } // MkdirAll like mkdir -p, but for webdav @@ -310,13 +292,13 @@ func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { return } if status != 201 { - return newPathError("MkdirAll", sub, status) + return NewPathError("MkdirAll", sub, status) } } return nil } - return newPathError("MkdirAll", path, status) + return NewPathError("MkdirAll", path, status) } // Rename moves a file from A to B @@ -351,7 +333,7 @@ func (c *Client) Read(path string) ([]byte, error) { func (c *Client) ReadStream(path string) (io.ReadCloser, error) { rs, err := c.req("GET", path, nil, nil) if err != nil { - return nil, newPathErrorErr("ReadStream", path, err) + return nil, NewPathErrorErr("ReadStream", path, err) } if rs.StatusCode == 200 { @@ -359,7 +341,7 @@ func (c *Client) ReadStream(path string) (io.ReadCloser, error) { } 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, @@ -379,7 +361,7 @@ func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadClos } }) if err != nil { - return nil, newPathErrorErr("ReadStreamRange", path, err) + return nil, NewPathErrorErr("ReadStreamRange", path, err) } if rs.StatusCode == http.StatusPartialContent { @@ -392,7 +374,7 @@ func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadClos if rs.StatusCode == 200 { // discard first 'offset' bytes. if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { - return nil, newPathErrorErr("ReadStreamRange", path, err) + return nil, NewPathErrorErr("ReadStreamRange", path, err) } // return a io.ReadCloser that is limited to `length` bytes. @@ -400,7 +382,7 @@ func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadClos } rs.Body.Close() - return nil, newPathError("ReadStream", path, rs.StatusCode) + return nil, NewPathError("ReadStream", path, rs.StatusCode) } // Write writes data to a given path @@ -430,7 +412,7 @@ func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) { } } - return newPathError("Write", path, s) + return NewPathError("Write", path, s) } // WriteStream writes a stream @@ -451,6 +433,6 @@ func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode) (err return nil default: - return newPathError("WriteStream", path, s) + return NewPathError("WriteStream", path, s) } } diff --git a/client_test.go b/client_test.go index 9195b19..ac2dd99 100644 --- a/client_test.go +++ b/client_test.go @@ -17,6 +17,12 @@ import ( "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 { @@ -55,18 +61,27 @@ func fillFs(t *testing.T, fs webdav.FileSystem) context.Context { } func newServer(t *testing.T) (*Client, *httptest.Server, webdav.FileSystem, context.Context) { - mux := http.NewServeMux() - fs := webdav.NewMemFS() - ctx := fillFs(t, fs) - mux.HandleFunc("/", basicAuth(&webdav.Handler{ - FileSystem: fs, - LockSystem: webdav.NewMemLS(), - })) - srv := httptest.NewServer(mux) + 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() @@ -80,6 +95,36 @@ func TestConnect(t *testing.T) { } } +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() @@ -135,6 +180,24 @@ func TestRead(t *testing.T) { } } +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() diff --git a/cmd/gowebdav/main.go b/cmd/gowebdav/main.go index ef1b7ed..bc1a4fc 100644 --- a/cmd/gowebdav/main.go +++ b/cmd/gowebdav/main.go @@ -55,6 +55,10 @@ func main() { c := d.NewClient(*root, *user, *password) + if e := c.Connect(); e != nil { + panic(e) + } + cmd := getCmd(*method) if e := cmd(c, flag.Arg(0), flag.Arg(1)); e != nil { diff --git a/digestAuth.go b/digestAuth.go index d171670..856d280 100644 --- a/digestAuth.go +++ b/digestAuth.go @@ -17,32 +17,46 @@ type DigestAuth struct { digestParts map[string]string } -// Type identifies the DigestAuthenticator -func (d *DigestAuth) Type() string { - 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 +// NewDigestAuth creates a new instance of our Digest Authenticator +func NewDigestAuth(login, secret string, rs *http.Response) (Authenticator, error) { + return &DigestAuth{login, secret, digestParts(rs)}, nil } // Authorize the current request -func (d *DigestAuth) Authorize(req *http.Request, method string, path string) { - parts := make(map[string]string, len(d.digestParts)+4) +func (d *DigestAuth) Authorize(c *http.Client, rq *http.Request, path string) error { + d.digestParts["uri"] = path + d.digestParts["method"] = rq.Method + d.digestParts["username"] = d.user + d.digestParts["password"] = d.pw + rq.Header.Set("Authorization", getDigestAuthorization(d.digestParts)) + return nil +} + +// Verify checks for authentication issues and may trigger a re-authentication +func (d *DigestAuth) Verify(c *http.Client, rs *http.Response, path string) (redo bool, err error) { + if rs.StatusCode == 401 { + err = NewPathError("Authorize", path, rs.StatusCode) + } + return +} + +// Close cleans up all resources +func (d *DigestAuth) Close() error { + return nil +} + +// Clone creates a copy of itself +func (d *DigestAuth) Clone() Authenticator { + parts := make(map[string]string, len(d.digestParts)) for k, v := range d.digestParts { parts[k] = v } - parts["uri"] = path - parts["method"] = method - parts["username"] = d.user - parts["password"] = d.pw - req.Header.Set("Authorization", getDigestAuthorization(parts)) + return &DigestAuth{d.user, d.pw, parts} +} + +// String toString +func (d *DigestAuth) String() string { + return fmt.Sprintf("DigestAuth login: %s", d.user) } func digestParts(resp *http.Response) map[string]string { diff --git a/digestAuth_test.go b/digestAuth_test.go new file mode 100644 index 0000000..6755151 --- /dev/null +++ b/digestAuth_test.go @@ -0,0 +1,35 @@ +package gowebdav + +import ( + "net/http" + "strings" + "testing" +) + +func TestNewDigestAuth(t *testing.T) { + a := &DigestAuth{"user", "password", 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", "password", make(map[string]string, 0)} + rq, _ := http.NewRequest("GET", "http://localhost/", nil) + a.Authorize(nil, rq, "/") + // TODO this is a very lazy test it cuts of cnonce + ex := `Digest username="user", realm="", nonce="", uri="/", nc=1, cnonce="` + if strings.Index(rq.Header.Get("Authorization"), ex) != 0 { + t.Error("got wrong Authorization header: " + rq.Header.Get("Authorization")) + } +} diff --git a/errors.go b/errors.go index bbf1e92..afc1af6 100644 --- a/errors.go +++ b/errors.go @@ -1,10 +1,18 @@ 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 { @@ -32,7 +40,7 @@ func IsErrNotFound(err error) bool { return IsErrCode(err, 404) } -func newPathError(op string, path string, statusCode int) error { +func NewPathError(op string, path string, statusCode int) error { return &os.PathError{ Op: op, Path: path, @@ -40,7 +48,7 @@ func newPathError(op string, path string, statusCode int) error { } } -func newPathErrorErr(op string, path string, err error) error { +func NewPathErrorErr(op string, path string, err error) error { return &os.PathError{ Op: op, Path: path, diff --git a/requests.go b/requests.go index 0966d1a..c8619ad 100644 --- a/requests.go +++ b/requests.go @@ -1,7 +1,6 @@ package gowebdav import ( - "bytes" "io" "log" "net/http" @@ -9,83 +8,54 @@ import ( "strings" ) -func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (req *http.Response, err error) { +func (c *Client) req(method, path string, body io.Reader, intercept func(*http.Request)) (rs *http.Response, err error) { + var redo bool var r *http.Request - var retryBuf io.Reader + var uri = PathEscape(Join(c.root, path)) + auth, body := c.auth.NewAuthenticator(body) + defer auth.Close() - 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 sk, ok := body.(io.Seeker); ok { - if _, err = sk.Seek(0, io.SeekStart); err != nil { - return + for { // TODO auth.continue() strategy(true|n times|until)? + if r, err = http.NewRequest(method, uri, body); err != nil { + return + } + + for k, vals := range c.headers { + for _, v := range vals { + r.Header.Add(k, v) } - 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) } - // retryBuf will be nil if body was nil initially so no check - // for body == nil is required here. - return c.req(method, path, retryBuf, intercept) - } else if rs.StatusCode == 401 { - return rs, newPathError("Authorize", c.root, rs.StatusCode) + if err = auth.Authorize(c.c, r, path); err != nil { + return + } + + if intercept != nil { + intercept(r) + } + + if c.interceptor != nil { + c.interceptor(method, r) + } + + if rs, err = c.c.Do(r); err != nil { + return + } + + if redo, err = auth.Verify(c.c, rs, path); err != nil { + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + return nil, err + } + if redo { + io.Copy(io.Discard, rs.Body) + rs.Body.Close() + if body, err = r.GetBody(); err != nil { + return nil, err + } + continue + } + break } return rs, err @@ -131,7 +101,7 @@ func (c *Client) propfind(path string, self bool, body string, resp interface{}, defer rs.Body.Close() if rs.StatusCode != 207 { - return newPathError("PROPFIND", path, rs.StatusCode) + return NewPathError("PROPFIND", path, rs.StatusCode) } return parseXML(rs.Body, resp, parse) @@ -189,7 +159,7 @@ func (c *Client) copymove(method string, oldpath string, newpath string, overwri 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) {