Files
git-pages/src/auth.go

94 lines
2.6 KiB
Go

package main
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net"
"net/http"
"os"
"slices"
"strings"
)
func GetHost(r *http.Request) string {
// FIXME: handle IDNA
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// dirty but the go stdlib doesn't have a "split port if present" function
host = r.Host
}
return host
}
func Authorize(w http.ResponseWriter, r *http.Request) error {
host := GetHost(r)
if os.Getenv("INSECURE") != "" {
return nil // for testing only
}
authorization := r.Header.Get("Authorization")
if authorization == "" {
http.Error(w, "missing Authorization header", http.StatusUnauthorized)
return fmt.Errorf("missing Authorization header")
}
scheme, param, success := strings.Cut(authorization, " ")
if !success {
http.Error(w, "malformed Authorization header", http.StatusBadRequest)
return fmt.Errorf("malformed Authorization header")
}
if scheme != "Pages" && scheme != "Basic" {
http.Error(w, "unknown Authorization scheme", http.StatusBadRequest)
return fmt.Errorf("unknown Authorization scheme")
}
// services like GitHub and Gogs cannot send a custom Authorization: header, but supplying
// username and password in the URL is basically just as good
if scheme == "Basic" {
basicParam, err := base64.StdEncoding.DecodeString(param)
if err != nil {
http.Error(w, "malformed Authorization: Basic header", http.StatusBadRequest)
return fmt.Errorf("malformed Authorization: Basic header")
}
username, password, found := strings.Cut(string(basicParam), ":")
if !found {
http.Error(w, "malformed Authorization: Basic parameter", http.StatusBadRequest)
return fmt.Errorf("malformed Authorization: Basic parameter")
}
if username != "Pages" {
http.Error(w, "unexpected Authorization: Basic username", http.StatusUnauthorized)
return fmt.Errorf("unexpected Authorization: Basic username")
}
param = password
}
challengeHostname := fmt.Sprintf("_git-pages-challenge.%s", host)
actualChallenges, err := net.LookupTXT(challengeHostname)
if err != nil {
http.Error(w, "failed to look up DNS challenge", http.StatusUnauthorized)
return fmt.Errorf("failed to look up %s: %s", challengeHostname, err)
}
expectedChallenge := fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%s %s", host, param)))
if !slices.Contains(actualChallenges, expectedChallenge) {
http.Error(w,
fmt.Sprintf("defeated by DNS challenge (%s not in %s)", expectedChallenge, challengeHostname),
http.StatusUnauthorized,
)
return fmt.Errorf(
"challenge mismatch for %s: %s does not contain %s",
challengeHostname,
actualChallenges,
expectedChallenge,
)
}
return nil
}