feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// Package auth handles HTTP authentication schemes (Basic, Digest, OAuth 2.0).
|
||||
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DigestAuthHandler handles HTTP Digest Authentication
|
||||
type DigestAuthHandler struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// NewDigestAuthHandler creates new handler
|
||||
func NewDigestAuthHandler(username, password string) *DigestAuthHandler {
|
||||
return &DigestAuthHandler{username, password}
|
||||
}
|
||||
|
||||
// HandleResponse processes a 401 response and returns a modified request
|
||||
func (h *DigestAuthHandler) HandleResponse(req *http.Request, resp *http.Response) (*http.Request, error) {
|
||||
// Parse WWW-Authenticate header
|
||||
authHeader := resp.Header.Get("WWW-Authenticate")
|
||||
if !strings.HasPrefix(authHeader, "Digest ") {
|
||||
return nil, fmt.Errorf("not a digest auth challenge")
|
||||
}
|
||||
|
||||
// Parse digest parameters
|
||||
params := parseDigestParams(authHeader[7:])
|
||||
|
||||
// Validate required parameters
|
||||
requiredParams := []string{"realm", "nonce"}
|
||||
for _, param := range requiredParams {
|
||||
if _, exists := params[param]; !exists {
|
||||
return nil, fmt.Errorf("missing required digest parameter: %s", param)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate response hash according to RFC 2617 / RFC 7616
|
||||
qop := params.get("qop")
|
||||
nonceCount := params.get("nc")
|
||||
cnonce := params.get("cnonce")
|
||||
opaque := params.get("opaque")
|
||||
|
||||
// HA1 = MD5(username:realm:password) or MD5(username:realm:password):nonce:cnonce
|
||||
ha1 := h.calculateHA1(params["realm"], params["nonce"], cnonce, qop)
|
||||
|
||||
// HA2 = MD5(method:uri) or MD5(method:uri:response-body) for auth-int.
|
||||
// RFC 7616 §3.4 (and RFC 2617 §3.2.2.3) require uri to be the
|
||||
// request-uri — the path *and* the query string, exactly as it
|
||||
// appears in the request line. Using req.URL.Path strips the
|
||||
// query, which causes digest auth to fail for any URL that carries
|
||||
// a token or other parameter in the query string.
|
||||
ha2 := h.calculateHA2(req.Method, req.URL.RequestURI(), qop)
|
||||
|
||||
// Response calculation based on qop
|
||||
var response string
|
||||
if qop == "auth" || qop == "auth-int" {
|
||||
response = md5Sum(ha1 + ":" + params["nonce"] + ":" + nonceCount + ":" + cnonce + ":" + qop + ":" + ha2)
|
||||
} else {
|
||||
// No qop (old RFC 2617 style)
|
||||
response = md5Sum(ha1 + ":" + params["nonce"] + ":" + ha2)
|
||||
}
|
||||
|
||||
// Build Authorization header — see note above about request-uri
|
||||
// vs path.
|
||||
auth := h.buildAuthHeader(params, req.URL.RequestURI(), response, nonceCount, cnonce, qop, opaque)
|
||||
|
||||
// Clone request and add header
|
||||
newReq := req.Clone(req.Context())
|
||||
newReq.Header.Set("Authorization", auth)
|
||||
|
||||
return newReq, nil
|
||||
}
|
||||
|
||||
func (h *DigestAuthHandler) calculateHA1(realm, nonce, cnonce, qop string) string {
|
||||
a1 := h.username + ":" + realm + ":" + h.password
|
||||
ha1 := md5Sum(a1)
|
||||
|
||||
// If qop is specified, we need to include nonce and cnonce
|
||||
if qop == "auth" || qop == "auth-int" {
|
||||
ha1 = md5Sum(ha1 + ":" + nonce + ":" + cnonce)
|
||||
}
|
||||
|
||||
return ha1
|
||||
}
|
||||
|
||||
func (h *DigestAuthHandler) calculateHA2(method, uri, qop string) string {
|
||||
a2 := method + ":" + uri
|
||||
// For auth-int, we would need to include body hash, but that's complex
|
||||
// For now, we just support auth and no-qop modes
|
||||
return md5Sum(a2)
|
||||
}
|
||||
|
||||
func (h *DigestAuthHandler) buildAuthHeader(params map[string]string, uri, response, nc, cnonce, qop, opaque string) string {
|
||||
parts := []string{
|
||||
fmt.Sprintf(`username="%s"`, h.username),
|
||||
fmt.Sprintf(`realm="%s"`, params["realm"]),
|
||||
fmt.Sprintf(`nonce="%s"`, params["nonce"]),
|
||||
fmt.Sprintf(`uri="%s"`, uri),
|
||||
fmt.Sprintf(`response="%s"`, response),
|
||||
}
|
||||
|
||||
if qop != "" {
|
||||
parts = append(parts,
|
||||
fmt.Sprintf(`qop=%s`, qop),
|
||||
fmt.Sprintf(`nc=%s`, nc),
|
||||
fmt.Sprintf(`cnonce="%s"`, cnonce),
|
||||
)
|
||||
}
|
||||
|
||||
if opaque != "" {
|
||||
parts = append(parts, fmt.Sprintf(`opaque="%s"`, opaque))
|
||||
}
|
||||
|
||||
return "Digest " + strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// GenerateCnonce generates a cryptographically random client nonce
|
||||
func GenerateCnonce() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// Fallback should never happen on a functioning system
|
||||
// but we still produce a value rather than crash
|
||||
for i := range b {
|
||||
b[i] = byte(i)
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// parseDigestParams parses the WWW-Authenticate header parameters
|
||||
func parseDigestParams(header string) digestParams {
|
||||
params := make(digestParams)
|
||||
|
||||
// Handle quoted and unquoted values
|
||||
currentKey := ""
|
||||
currentVal := ""
|
||||
inQuotes := false
|
||||
hasSeenEquals := false
|
||||
|
||||
for i := 0; i < len(header); i++ {
|
||||
char := header[i]
|
||||
|
||||
switch {
|
||||
case char == '=' && !inQuotes:
|
||||
currentKey = strings.TrimSpace(currentKey)
|
||||
currentVal = ""
|
||||
hasSeenEquals = true
|
||||
case char == '"' && (i == 0 || header[i-1] != '\\'):
|
||||
inQuotes = !inQuotes
|
||||
case char == ',' && !inQuotes:
|
||||
if currentKey != "" {
|
||||
params[currentKey] = strings.Trim(strings.TrimSpace(currentVal), `"`)
|
||||
}
|
||||
currentKey = ""
|
||||
currentVal = ""
|
||||
hasSeenEquals = false
|
||||
case (char == ' ' || char == '\t') && !inQuotes && ((!hasSeenEquals && currentKey == "") || (hasSeenEquals && currentVal == "")):
|
||||
// Skip leading whitespace before a key or after '='
|
||||
default:
|
||||
if !hasSeenEquals {
|
||||
currentKey += string(char)
|
||||
} else {
|
||||
currentVal += string(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last parameter
|
||||
if currentKey != "" {
|
||||
params[currentKey] = strings.Trim(strings.TrimSpace(currentVal), `"`)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// digestParams is a map of digest authentication parameters
|
||||
type digestParams map[string]string
|
||||
|
||||
func (p digestParams) get(key string) string {
|
||||
if val, ok := p[key]; ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func md5Sum(s string) string {
|
||||
h := md5.Sum([]byte(s))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// SHA256 sum for future use (RFC 7616 supports SHA-256)
|
||||
func sha256Sum(s string) string {
|
||||
h := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
Reference in New Issue
Block a user