f2805894c8
Signed-off-by: Tianon Gravi <admwiggin@gmail.com>
237 lines
5.7 KiB
Go
237 lines
5.7 KiB
Go
package challenge
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Challenge carries information from a WWW-Authenticate response header.
|
|
// See RFC 2617.
|
|
type Challenge struct {
|
|
// Scheme is the auth-scheme according to RFC 2617
|
|
Scheme string
|
|
|
|
// Parameters are the auth-params according to RFC 2617
|
|
Parameters map[string]string
|
|
}
|
|
|
|
// Manager manages the challenges for endpoints.
|
|
// The challenges are pulled out of HTTP responses. Only
|
|
// responses which expect challenges should be added to
|
|
// the manager, since a non-unauthorized request will be
|
|
// viewed as not requiring challenges.
|
|
type Manager interface {
|
|
// GetChallenges returns the challenges for the given
|
|
// endpoint URL.
|
|
GetChallenges(endpoint url.URL) ([]Challenge, error)
|
|
|
|
// AddResponse adds the response to the challenge
|
|
// manager. The challenges will be parsed out of
|
|
// the WWW-Authenicate headers and added to the
|
|
// URL which was produced the response. If the
|
|
// response was authorized, any challenges for the
|
|
// endpoint will be cleared.
|
|
AddResponse(resp *http.Response) error
|
|
}
|
|
|
|
// NewSimpleManager returns an instance of
|
|
// Manger which only maps endpoints to challenges
|
|
// based on the responses which have been added the
|
|
// manager. The simple manager will make no attempt to
|
|
// perform requests on the endpoints or cache the responses
|
|
// to a backend.
|
|
func NewSimpleManager() Manager {
|
|
return &simpleManager{
|
|
Challenges: make(map[string][]Challenge),
|
|
}
|
|
}
|
|
|
|
type simpleManager struct {
|
|
sync.RWMutex
|
|
Challenges map[string][]Challenge
|
|
}
|
|
|
|
func normalizeURL(endpoint *url.URL) {
|
|
endpoint.Host = strings.ToLower(endpoint.Host)
|
|
endpoint.Host = canonicalAddr(endpoint)
|
|
}
|
|
|
|
func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
|
|
normalizeURL(&endpoint)
|
|
|
|
m.RLock()
|
|
defer m.RUnlock()
|
|
challenges := m.Challenges[endpoint.String()]
|
|
return challenges, nil
|
|
}
|
|
|
|
func (m *simpleManager) AddResponse(resp *http.Response) error {
|
|
challenges := ResponseChallenges(resp)
|
|
if resp.Request == nil {
|
|
return fmt.Errorf("missing request reference")
|
|
}
|
|
urlCopy := url.URL{
|
|
Path: resp.Request.URL.Path,
|
|
Host: resp.Request.URL.Host,
|
|
Scheme: resp.Request.URL.Scheme,
|
|
}
|
|
normalizeURL(&urlCopy)
|
|
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
m.Challenges[urlCopy.String()] = challenges
|
|
return nil
|
|
}
|
|
|
|
// Octet types from RFC 2616.
|
|
type octetType byte
|
|
|
|
var octetTypes [256]octetType
|
|
|
|
const (
|
|
isToken octetType = 1 << iota
|
|
isSpace
|
|
)
|
|
|
|
func init() {
|
|
// OCTET = <any 8-bit sequence of data>
|
|
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
// CR = <US-ASCII CR, carriage return (13)>
|
|
// LF = <US-ASCII LF, linefeed (10)>
|
|
// SP = <US-ASCII SP, space (32)>
|
|
// HT = <US-ASCII HT, horizontal-tab (9)>
|
|
// <"> = <US-ASCII double-quote mark (34)>
|
|
// CRLF = CR LF
|
|
// LWS = [CRLF] 1*( SP | HT )
|
|
// TEXT = <any OCTET except CTLs, but including LWS>
|
|
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
|
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
|
// token = 1*<any CHAR except CTLs or separators>
|
|
// qdtext = <any TEXT except <">>
|
|
|
|
for c := 0; c < 256; c++ {
|
|
var t octetType
|
|
isCtl := c <= 31 || c == 127
|
|
isChar := 0 <= c && c <= 127
|
|
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
|
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
|
t |= isSpace
|
|
}
|
|
if isChar && !isCtl && !isSeparator {
|
|
t |= isToken
|
|
}
|
|
octetTypes[c] = t
|
|
}
|
|
}
|
|
|
|
// ResponseChallenges returns a list of authorization challenges
|
|
// for the given http Response. Challenges are only checked if
|
|
// the response status code was a 401.
|
|
func ResponseChallenges(resp *http.Response) []Challenge {
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
// Parse the WWW-Authenticate Header and store the challenges
|
|
// on this endpoint object.
|
|
return parseAuthHeader(resp.Header)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseAuthHeader(header http.Header) []Challenge {
|
|
challenges := []Challenge{}
|
|
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
|
v, p := parseValueAndParams(h)
|
|
if v != "" {
|
|
challenges = append(challenges, Challenge{Scheme: v, Parameters: p})
|
|
}
|
|
}
|
|
return challenges
|
|
}
|
|
|
|
func parseValueAndParams(header string) (value string, params map[string]string) {
|
|
params = make(map[string]string)
|
|
value, s := expectToken(header)
|
|
if value == "" {
|
|
return
|
|
}
|
|
value = strings.ToLower(value)
|
|
s = "," + skipSpace(s)
|
|
for strings.HasPrefix(s, ",") {
|
|
var pkey string
|
|
pkey, s = expectToken(skipSpace(s[1:]))
|
|
if pkey == "" {
|
|
return
|
|
}
|
|
if !strings.HasPrefix(s, "=") {
|
|
return
|
|
}
|
|
var pvalue string
|
|
pvalue, s = expectTokenOrQuoted(s[1:])
|
|
if pvalue == "" {
|
|
return
|
|
}
|
|
pkey = strings.ToLower(pkey)
|
|
params[pkey] = pvalue
|
|
s = skipSpace(s)
|
|
}
|
|
return
|
|
}
|
|
|
|
func skipSpace(s string) (rest string) {
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
if octetTypes[s[i]]&isSpace == 0 {
|
|
break
|
|
}
|
|
}
|
|
return s[i:]
|
|
}
|
|
|
|
func expectToken(s string) (token, rest string) {
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
if octetTypes[s[i]]&isToken == 0 {
|
|
break
|
|
}
|
|
}
|
|
return s[:i], s[i:]
|
|
}
|
|
|
|
func expectTokenOrQuoted(s string) (value string, rest string) {
|
|
if !strings.HasPrefix(s, "\"") {
|
|
return expectToken(s)
|
|
}
|
|
s = s[1:]
|
|
for i := 0; i < len(s); i++ {
|
|
switch s[i] {
|
|
case '"':
|
|
return s[:i], s[i+1:]
|
|
case '\\':
|
|
p := make([]byte, len(s)-1)
|
|
j := copy(p, s[:i])
|
|
escape := true
|
|
for i = i + 1; i < len(s); i++ {
|
|
b := s[i]
|
|
switch {
|
|
case escape:
|
|
escape = false
|
|
p[j] = b
|
|
j++
|
|
case b == '\\':
|
|
escape = true
|
|
case b == '"':
|
|
return string(p[:j]), s[i+1:]
|
|
default:
|
|
p[j] = b
|
|
j++
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|