package digest import ( "errors" "fmt" "net/http" "strings" "github.com/icholy/digest/internal/param" ) // Challenge is a challenge sent in the WWW-Authenticate header type Challenge struct { Realm string Domain []string Nonce string Opaque string Stale bool Algorithm string QOP []string Charset string Userhash bool } // SupportsQOP returns true if the challenge advertises support // for the provided qop value func (c *Challenge) SupportsQOP(qop string) bool { for _, v := range c.QOP { if v == qop { return true } } return false } // ParseChallenge parses the WWW-Authenticate header challenge func ParseChallenge(s string) (*Challenge, error) { s, ok := strings.CutPrefix(s, Prefix) if !ok { return nil, errors.New("digest: invalid challenge prefix") } pp, err := param.Parse(s) if err != nil { return nil, fmt.Errorf("digest: invalid challenge: %w", err) } var c Challenge for _, p := range pp { switch p.Key { case "realm": c.Realm = p.Value case "domain": c.Domain = strings.Fields(p.Value) case "nonce": c.Nonce = p.Value case "algorithm": c.Algorithm = p.Value case "stale": c.Stale = strings.ToLower(p.Value) == "true" case "opaque": c.Opaque = p.Value case "qop": c.QOP = strings.Split(p.Value, ",") case "charset": c.Charset = p.Value case "userhash": c.Userhash = strings.ToLower(p.Value) == "true" } } return &c, nil } // String returns the foramtted header value func (c *Challenge) String() string { var pp []param.Param pp = append(pp, param.Param{ Key: "realm", Value: c.Realm, Quote: true, }) if len(c.Domain) != 0 { pp = append(pp, param.Param{ Key: "domain", Value: strings.Join(c.Domain, " "), Quote: true, }) } pp = append(pp, param.Param{ Key: "nonce", Value: c.Nonce, Quote: true, }) if c.Opaque != "" { pp = append(pp, param.Param{ Key: "opaque", Value: c.Opaque, Quote: true, }) } if c.Stale { pp = append(pp, param.Param{ Key: "stale", Value: "true", }) } if c.Algorithm != "" { pp = append(pp, param.Param{ Key: "algorithm", Value: c.Algorithm, }) } if len(c.QOP) != 0 { pp = append(pp, param.Param{ Key: "qop", Value: strings.Join(c.QOP, ","), Quote: true, }) } if c.Charset != "" { pp = append(pp, param.Param{ Key: "charset", Value: c.Charset, }) } if c.Userhash { pp = append(pp, param.Param{ Key: "userhash", Value: "true", }) } return Prefix + param.Format(pp...) } // ErrNoChallenge indicates that no WWW-Authenticate headers were found. var ErrNoChallenge = errors.New("digest: no challenge found") // FindChallenge returns the first supported challenge in the headers func FindChallenge(h http.Header) (*Challenge, error) { var last error for _, header := range h.Values("WWW-Authenticate") { if !IsDigest(header) { continue } chal, err := ParseChallenge(header) if err == nil && CanDigest(chal) { return chal, nil } if err != nil { last = err } } if last != nil { return nil, last } return nil, ErrNoChallenge }