| 1 | package auth |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "net/url" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/protocol/nostr" |
| 9 | ) |
| 10 | |
| 11 | // ErrGRASP08Unauthorized is the detail-free GRASP-08 authentication error |
| 12 | // returned for every failed repository proof. |
| 13 | var ErrGRASP08Unauthorized = errors.New("auth-required:" ) |
| 14 | |
| 15 | // VerifyGRASP08 validates a reusable kind 27235 GET proof for the repository |
| 16 | // root named by a GRASP-08 Smart HTTP request. The proof remains valid for |
| 17 | // one minute across the repository's supported Git requests. |
| 18 | func (v *Validator) VerifyGRASP08(header, rawURL string) (nostr.Event, error) { |
| 19 | e, err := decodeToken(header, "GRASP-08" ) |
| 20 | if err != nil { |
| 21 | return nostr.Event{}, ErrGRASP08Unauthorized |
| 22 | } |
| 23 | if err := nostr.Validate(e); err != nil || e.Kind != 27235 { |
| 24 | return nostr.Event{}, ErrGRASP08Unauthorized |
| 25 | } |
| 26 | now := v.now() |
| 27 | if e.CreatedAt < now.Unix()-60 || e.CreatedAt > now.Unix()+60 { |
| 28 | return nostr.Event{}, ErrGRASP08Unauthorized |
| 29 | } |
| 30 | if nostr.Tag(e, "method" ) != "GET" { |
| 31 | return nostr.Event{}, ErrGRASP08Unauthorized |
| 32 | } |
| 33 | root, ok := GRASP08RepositoryRoot(rawURL) |
| 34 | if !ok || sameRequestURL(nostr.Tag(e, "u" ), root) != nil { |
| 35 | return nostr.Event{}, ErrGRASP08Unauthorized |
| 36 | } |
| 37 | return e, nil |
| 38 | } |
| 39 | |
| 40 | // GRASP08RepositoryRoot returns the canonical repository URL for a supported |
| 41 | // Smart HTTP request beneath a .git path. |
| 42 | func GRASP08RepositoryRoot(raw string) (string, bool) { |
| 43 | u, err := url.Parse(raw) |
| 44 | if err != nil || (u.Scheme != "http" && u.Scheme != "https" ) || u.Host == "" || u.User != nil { |
| 45 | return "" , false |
| 46 | } |
| 47 | // Keep the client's percent-encoding so the root compares equal to the |
| 48 | // signed u tag, which is also kept as sent. |
| 49 | path := u.EscapedPath() |
| 50 | i := strings.LastIndex(path, ".git" ) |
| 51 | if i < 0 || (i+4 < len(path) && path[i+4] != '/' ) { |
| 52 | return "" , false |
| 53 | } |
| 54 | rootPath := path[:i+4] |
| 55 | suffix := path[i+4:] |
| 56 | if suffix != "" && suffix != "/info/refs" && suffix != "/git-upload-pack" && suffix != "/git-receive-pack" { |
| 57 | return "" , false |
| 58 | } |
| 59 | if strings.HasSuffix(rootPath, "/" ) { |
| 60 | return "" , false |
| 61 | } |
| 62 | unescaped, err := url.PathUnescape(rootPath) |
| 63 | if err != nil { |
| 64 | return "" , false |
| 65 | } |
| 66 | u.Path, u.RawPath, u.RawQuery, u.Fragment = unescaped, rootPath, "" , "" |
| 67 | return u.String(), true |
| 68 | } |