-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
The main goal was to simplify the `req` method in `request.go` and making it easier to add more authentication methods. All the magic went into the `auth.go` file. This feature introduces an `Authorizer` which acts as an `Authenticator` factory. Under the hood it creates an authenticator shim per request, which delegates the authentication flow to our authenticators. The authentication flow itself is broken down into `Authorize' and `Verify' steps to encapsulate and control complex authentication challenges. Furthermore, the default `NewAutoAuth' authenticator can be overridden by a custom implementation for more control over flow and resources. The `NewBacisAuth` Authorizer gives us the feel of the old days.
- Loading branch information
Showing
6 changed files
with
458 additions
and
232 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package gowebdav | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"sync" | ||
) | ||
|
||
// AlgoChangedErr must be thrown from the Verify method | ||
// to trigger a re-authentication with a new algorithm. | ||
type AlgoChangedErr struct{} | ||
|
||
func (e AlgoChangedErr) Error() string { | ||
return "AuthChangedErr" | ||
} | ||
|
||
// AuthFactory prototype function to create a new Authenticator | ||
type AuthFactory func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) | ||
|
||
// Authorizer a Authenticator factory | ||
type Authorizer interface { | ||
NewAuthenticator(body io.Reader) (Authenticator, io.Reader) | ||
AddAuthenticator(key string, fn AuthFactory) | ||
} | ||
|
||
// Authenticator stub | ||
type Authenticator interface { | ||
Authorize(c *http.Client, rq *http.Request, method string, path string) error | ||
Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) | ||
Clone() Authenticator | ||
io.Closer | ||
} | ||
|
||
// authorizer structure holds our Authenticator create functions | ||
type authorizer struct { | ||
factories map[string]AuthFactory | ||
defAuthMux sync.Mutex | ||
defAuth Authenticator | ||
} | ||
|
||
// authShim structure that wraps the real Authenticator | ||
type authShim struct { | ||
factory AuthFactory | ||
body io.Reader | ||
auth Authenticator | ||
} | ||
|
||
// nullAuth initializes the whole authentication flow | ||
type nullAuth struct{} | ||
|
||
// NewAutoAuth creates an auto Authenticator factory | ||
func NewAutoAuth(login string, secret string) Authorizer { | ||
fmap := make(map[string]AuthFactory) | ||
az := &authorizer{fmap, sync.Mutex{}, &nullAuth{}} | ||
|
||
az.AddAuthenticator("basic", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) { | ||
return &BasicAuth{login, secret}, nil | ||
}) | ||
|
||
az.AddAuthenticator("digest", func(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) { | ||
return &DigestAuth{login, secret, digestParts(rs)}, nil | ||
}) | ||
|
||
return az | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
func (a *authorizer) AddAuthenticator(key string, fn AuthFactory) { | ||
a.factories[key] = fn | ||
} | ||
|
||
// factory creates an Authenticator instance based on the WWW-Authenticate header | ||
func (a *authorizer) factory(rq *http.Request, rs *http.Response, method, path string) (auth Authenticator, err error) { | ||
header := strings.ToLower(rs.Header.Get("Www-Authenticate")) | ||
for k, fn := range a.factories { | ||
if strings.Contains(header, k) { | ||
if auth, err = fn(rq, rs, method, path); err != nil { | ||
return | ||
} | ||
break | ||
} | ||
} | ||
if auth == nil { | ||
return nil, newPathError("NoAuthenticator", path, rs.StatusCode) | ||
} | ||
|
||
a.defAuthMux.Lock() | ||
a.defAuth = auth | ||
a.defAuthMux.Unlock() | ||
|
||
return auth, nil | ||
} | ||
|
||
// Authorize the current request | ||
func (s *authShim) Authorize(c *http.Client, rq *http.Request, method string, path string) error { | ||
if err := s.auth.Authorize(c, rq, method, 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(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) { | ||
reauth, err = s.auth.Verify(rq, rs, method, path) | ||
if err != nil { | ||
if _, ok := err.(AlgoChangedErr); ok { | ||
if auth, aerr := s.factory(rq, rs, method, path); aerr == nil { | ||
s.auth = auth | ||
return true, nil | ||
} else { | ||
err = aerr | ||
} | ||
} | ||
} | ||
return | ||
} | ||
|
||
// Close closes all resources | ||
func (s *authShim) Close() error { | ||
if s.body != nil { | ||
if closer, ok := s.body.(io.Closer); ok { | ||
return closer.Close() | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Clone creates a copy of itself | ||
func (s *authShim) Clone() Authenticator { | ||
// panic? | ||
return nil | ||
} | ||
|
||
// String toString | ||
func (s *authShim) String() string { | ||
return "AuthShim" | ||
} | ||
|
||
// Authorize the current request | ||
func (n *nullAuth) Authorize(c *http.Client, rq *http.Request, method string, path string) error { | ||
return nil | ||
} | ||
|
||
// Verify checks for authentication issues and may trigger a re-authentication | ||
func (n *nullAuth) Verify(rq *http.Request, rs *http.Response, method string, path string) (reauth bool, err error) { | ||
return true, AlgoChangedErr{} | ||
} | ||
|
||
// 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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.