mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-16 20:21:33 +00:00
Add authorization.
This commit is contained in:
@@ -49,6 +49,12 @@ $ curl http://127.0.0.1:3333/ -H 'Host: codeberg.page'
|
||||
```
|
||||
|
||||
|
||||
Authorization
|
||||
-------------
|
||||
|
||||
DNS is used for authorization of content updates for custom domain names. Whenever a `PUT` or `POST` request is received at `hostname.tld` that has an `Authorization: Pages <token>` header, the TXT record(s) at `_git-pages-challenge.hostname.tld` are compared with `sha256("hostname.tld <token>")`. If there is a match then updates from any clone URLs are allowed.
|
||||
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
@@ -66,8 +72,6 @@ This approach has the benefits of being easy to explore and debug, but places a
|
||||
|
||||
The specific arrangement used is clearly not optimal; at a minimum it is likely worth it to deduplicate files under `data/tree/` using hardlinks, or perhaps to put objects in a flat, content-addressed store with `data/www/` linking to each individual file. The key practical constraint will likely be the need to attribute excessively large trees to repositories they were built from (and to perform GC), which suggests adding structure and not removing it.
|
||||
|
||||
I lack any interesting insight into authentication mechanisms applicable here. It would be straightforward to verify whether a custom domain contains a TXT record specifying the allowed source repositories.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
64
auth.go
Normal file
64
auth.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"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)
|
||||
|
||||
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" {
|
||||
http.Error(w, "unknown Authorization scheme", http.StatusBadRequest)
|
||||
return fmt.Errorf("unknown Authorization scheme")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
41
serve.go
41
serve.go
@@ -2,15 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -23,16 +19,6 @@ import (
|
||||
|
||||
const fetchTimeout = 30 * time.Second
|
||||
|
||||
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 getPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
|
||||
host := getHost(r)
|
||||
|
||||
@@ -120,6 +106,11 @@ func getProjectName(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
func putPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
|
||||
host := getHost(r)
|
||||
|
||||
err := authorize(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectName, err := getProjectName(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,11 +155,14 @@ func putPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
|
||||
return result.err
|
||||
}
|
||||
|
||||
const hmacSecret = ""
|
||||
|
||||
func postPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
|
||||
host := getHost(r)
|
||||
|
||||
err := authorize(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectName, err := getProjectName(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -189,21 +183,6 @@ func postPage(dataDir string, w http.ResponseWriter, r *http.Request) error {
|
||||
return fmt.Errorf("body read: %s", err)
|
||||
}
|
||||
|
||||
if hmacSecret != "" {
|
||||
signature, err := hex.DecodeString(r.Header.Get("X-Forgejo-Signature"))
|
||||
if err != nil {
|
||||
http.Error(w, "malformed signature", http.StatusBadRequest)
|
||||
return fmt.Errorf("malformed signature")
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(hmacSecret))
|
||||
mac.Write(requestBody)
|
||||
if !hmac.Equal(mac.Sum(nil), signature) {
|
||||
http.Error(w, "invalid signature", http.StatusBadRequest)
|
||||
return fmt.Errorf("invalid hmac")
|
||||
}
|
||||
}
|
||||
|
||||
var event map[string]any
|
||||
err = json.NewDecoder(bytes.NewReader(requestBody)).Decode(&event)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user