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
This commit is contained in:
Christoph Polcin
2023-02-03 10:18:35 +01:00
committed by Christoph Polcin
parent 3282f94193
commit ca40e2802e
12 changed files with 994 additions and 285 deletions

View File

@@ -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)
}
}