From 7eac1db4758510d2db6ba0d8d8c85db6784e31f6 Mon Sep 17 00:00:00 2001 From: mstar Date: Wed, 16 Apr 2025 16:43:57 +0200 Subject: [PATCH] Start work on auth fetch middleware --- activitypub/import.go | 6 +- temp.toml | 2 +- web/public/errorpages.go | 4 +- web/public/middleware/authFetchCheck.go | 104 ++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 web/public/middleware/authFetchCheck.go diff --git a/activitypub/import.go b/activitypub/import.go index 67403fd..3e3d3f6 100644 --- a/activitypub/import.go +++ b/activitypub/import.go @@ -77,7 +77,7 @@ func ImportRemoteAccount(targetName string) (string, error) { } defer response.Body.Close() body, _ := io.ReadAll(response.Body) - log.Trace(). + log.Debug(). Int("status", response.StatusCode). Bytes("body", body). Any("headers", response.Header). @@ -394,7 +394,7 @@ func ImportRemoteServer(host string) (uint, error) { } } } - if nodeAdmins, ok := data.Metadata["nodeAdmins"].([]map[string]any); ok { + if nodeAdmins, ok := data.Metadata["nodeAdmins"].([]map[string]any); ok && len(nodeAdmins) > 0 { log.Debug().Msg("Node admins url found") targets := sliceutils.Filter( existingEntry.Metadata, @@ -530,7 +530,7 @@ func ImportRemoteServer(host string) (uint, error) { } } } - if staffAccounts, ok := data.Metadata["nodeAdmins"].([]any); ok { + if staffAccounts, ok := data.Metadata["nodeAdmins"].([]any); ok && len(staffAccounts) > 0 { log.Debug().Msg("Node admins url found") targets := sliceutils.Filter( existingEntry.Metadata, diff --git a/temp.toml b/temp.toml index fcab56e..8efde9d 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,7 @@ [general] protocol = "https" domain = "lhr.life" - subdomain = "47565bb39100de" + subdomain = "3e4af10addc7d0" private_port = 8080 public_port = 443 diff --git a/web/public/errorpages.go b/web/public/errorpages.go index 2363848..e377765 100644 --- a/web/public/errorpages.go +++ b/web/public/errorpages.go @@ -12,12 +12,14 @@ var errorDescriptions = map[string]string{ "db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time", "bad-request-data": "The data provided in the request doesn't match the requirements, see problem details' detail field for more information", "bad-page": "The provided page number was not valid. See response details for more information", + "bad-accept-mime-type": "The requested mime type is not supported for the target url", + "invalid-auth-signature": "The requested URL requires verification via authorized fetch. See https://swicg.github.io/activitypub-http-signature/ and https://datatracker.ietf.org/doc/html/rfc9421 (not implemented yet) for details on how to verify your request", } func errorTypeHandler(w http.ResponseWriter, r *http.Request) { errName := r.PathValue("name") if description, ok := errorDescriptions[errName]; ok { - fmt.Fprint(w, description) + _, _ = fmt.Fprint(w, description) } else { webutils.ProblemDetailsStatusOnly(w, 404) } diff --git a/web/public/middleware/authFetchCheck.go b/web/public/middleware/authFetchCheck.go new file mode 100644 index 0000000..7c7a0d3 --- /dev/null +++ b/web/public/middleware/authFetchCheck.go @@ -0,0 +1,104 @@ +package webmiddleware + +import ( + "net/http" + "net/url" + "regexp" + + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/other" +) + +const signatureRegexString = `keyId=([a-zA-Z0-9_\-\.:@/\?&=#%\+\[\]!$\(\)\*,;]+),headers="([a-z0-9-_\(\) ]+)",(?:algorithm="([a-z0-9-])+",)?signature="(.+)"` + +var publicPaths = []*regexp.Regexp{ + regexp.MustCompile(`/\.well-known/.+^`), + // regexp.MustCompile(``), +} + +var signatureRegex = regexp.MustCompile(signatureRegexString) + +// Builder for the authorized fetch check middleware. +// forNonGet determines whether the check should happen for non-GET requests. +// forGet determines whether the check should also happen for GET requests. +// forGet being true implicitly sets forNonGet true too. +// Requests to the server actor and other, required public resources +// will never be checked +func BuildAuthorizedFetchCheck(forNonGet bool, forGet bool) webutils.HandlerBuilder { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + // Check always open path first + for _, re := range publicPaths { + if re.Match([]byte(path)) { + h.ServeHTTP(w, r) + return + } + } + // Not an always open path, check methods + if r.Method == "GET" && !forGet { + h.ServeHTTP(w, r) + return + } else if !forGet && !forNonGet { + h.ServeHTTP(w, r) + return + } + // TODO: Perform check here + signatureHeader := r.Header.Get("Signature") + if signatureHeader == "" { + webutils.ProblemDetails( + w, + http.StatusUnauthorized, + "/errors/invalid-auth-signature", + "invalid authorization signature", + other.IntoPointer("Missing Signature header for authorization"), + nil, + ) + return + } + match := signatureRegex.FindStringSubmatch(signatureHeader) + if len(match) <= 1 { + webutils.ProblemDetails( + w, + http.StatusUnauthorized, + "/errors/invalid-auth-signature", + "invalid authorization signature", + other.IntoPointer("Invalid signature header format"), + map[string]any{ + "used signature regex": signatureRegexString, + "signature format": `keyId="",headers="",(optional: algorithm="", (defaults to rsa-sha256 if not set))signature="signed message"`, + }, + ) + return + } + // fullMatch = match[0] + rawKeyId := match[1] + rawHeaders := match[2] + rawAlgorithm := match[3] + signature := match[4] + + _, err := url.Parse(rawKeyId) + if err != nil { + webutils.ProblemDetails( + w, + http.StatusUnauthorized, + "/errors/invalid-auth-signature", + "invalid authorization signature", + other.IntoPointer("keyId must be a valid url"), + nil, + ) + return + } + + if rawAlgorithm == "" { + rawAlgorithm = "hs2019" + // w.Header().Add("X-Algorithm-Hint", "") + } + + _ = rawHeaders + _ = signature + _ = rawAlgorithm + panic("not implemented") + }) + } +}