Refactoring

This commit is contained in:
Melroy van den Berg 2025-04-24 23:23:43 +02:00
parent 5d08981a5f
commit 02853f346d
No known key found for this signature in database
GPG key ID: 71D11FF23454B9D7
2 changed files with 85 additions and 188 deletions

View file

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
@ -23,45 +24,42 @@ const (
// fetchActivityPubObjectWithSignature is a helper function that always signs HTTP requests
// This is the preferred way to fetch ActivityPub content as many instances require signatures
func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string, error) {
func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) ([]byte, map[string]interface{}, error) {
fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL)
// First, we need to extract the actor URL from the object URL
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
// Fetch the object itself
_, data, err := r.fetchActivityPubObjectDirect(objectURL)
if err != nil {
// If we can't extract the actor URL, fall back to a direct request
fmt.Printf("Could not extract actor URL: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL)
return nil, nil, err
}
// Then, we need to fetch the actor data to get the public key
actorData, err := r.fetchActorData(actorURL)
actorURL, ok := data["attributedTo"].(string)
if !ok || actorURL == "" {
return nil, nil, fmt.Errorf("could not find attributedTo in object")
}
// Fetch actor data
_, actorData, err := r.fetchActorData(actorURL)
if err != nil {
// If we can't fetch the actor data, fall back to a direct request
fmt.Printf("Could not fetch actor data: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL)
return nil, nil, fmt.Errorf("could not fetch actor data: %v", err)
}
// Extract the public key ID
keyID, _, err := r.extractPublicKey(actorData)
if err != nil {
// If we can't extract the public key, fall back to a direct request
fmt.Printf("Could not extract public key: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL)
return nil, nil, fmt.Errorf("could not extract public key: %v", err)
}
// Create a new private key for signing (in a real app, we would use a persistent key)
privateKey, err := generateRSAKey()
if err != nil {
// If we can't generate a key, fall back to a direct request
fmt.Printf("Could not generate RSA key: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL)
return nil, nil, fmt.Errorf("could not generate RSA key: %v", err)
}
// Now, sign and send the request
req, err := http.NewRequest("GET", objectURL, nil)
if err != nil {
return "", fmt.Errorf("error creating signed request: %v", err)
return nil, nil, fmt.Errorf("error creating signed request: %v", err)
}
// Set headers
@ -71,59 +69,45 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
// Sign the request
if err := signRequest(req, keyID, privateKey); err != nil {
// If we can't sign the request, fall back to a direct request
fmt.Printf("Could not sign request: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL)
return nil, nil, fmt.Errorf("could not sign request: %v", err)
}
// Send the request
fmt.Printf("Sending signed request with headers: %v\n", req.Header)
resp, err := r.client.Do(req)
if err != nil {
return "", fmt.Errorf("error sending signed request: %v", err)
return nil, nil, fmt.Errorf("error sending signed request: %v", err)
}
defer resp.Body.Close()
fmt.Printf("Received response with status: %s\n", resp.Status)
if resp.StatusCode != http.StatusOK {
// If the signed request fails, try a direct request as a fallback
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
fmt.Println("Signed request failed with auth error, trying direct request as fallback")
return r.fetchActivityPubObjectDirect(objectURL)
}
// Read body for error info
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
return nil, nil, fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
}
// Read and parse the response
body, err := io.ReadAll(resp.Body)
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response: %v", err)
return nil, nil, fmt.Errorf("error reading response: %v", err)
}
if len(bodyBytes) == 0 {
return nil, nil, fmt.Errorf("received empty response body")
}
// Debug output
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
// Check if the response is empty
if len(body) == 0 {
return "", fmt.Errorf("received empty response body")
}
// Remove ANSI escape codes if present (some servers return colored output)
cleanBody := removeANSIEscapeCodes(bodyBytes)
// Try to decode the JSON response
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("error decoding response: %v", err)
var body map[string]interface{}
if err := json.Unmarshal(cleanBody, &body); err != nil {
return nil, nil, fmt.Errorf("error decoding signed response: %v", err)
}
// Format the result
return formatter.Format(data)
return bodyBytes, body, nil
}
// fetchActivityPubObjectDirect is a helper function to fetch content without signatures
// This is used as a fallback when signing fails
func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error) {
func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) ([]byte, map[string]interface{}, error) {
fmt.Printf("Fetching ActivityPub object directly from: %s\n", objectURL)
// Create a custom client that doesn't follow redirects automatically
@ -137,18 +121,18 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
// Create the request
req, err := http.NewRequest("GET", objectURL, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
return nil, nil, fmt.Errorf("error creating request: %v", err)
}
// Set Accept headers to request ActivityPub data
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
req.Header.Set("User-Agent", UserAgent)
// Perform the request
fmt.Printf("Sending direct request with headers: %v\n", req.Header)
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error fetching content: %v", err)
return nil, nil, fmt.Errorf("error fetching content: %v", err)
}
defer resp.Body.Close()
@ -169,109 +153,43 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
if resp.StatusCode != http.StatusOK {
// Read body for error info
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body))
return nil, nil, fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body))
}
// Read and parse the response
body, err := io.ReadAll(resp.Body)
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response: %v", err)
return nil, nil, fmt.Errorf("error reading response: %v", err)
}
// Debug output
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
// Check if the response is empty
if len(body) == 0 {
return "", fmt.Errorf("received empty response body")
if len(bodyBytes) == 0 {
return nil, nil, fmt.Errorf("received empty response body")
}
// Remove ANSI escape codes if present (some servers return colored output)
cleanBody := removeANSIEscapeCodes(bodyBytes)
// Try to decode the JSON response
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("error decoding response: %v", err)
var body map[string]interface{}
if err := json.Unmarshal(cleanBody, &body); err != nil {
return nil, nil, fmt.Errorf("error decoding response: %v", err)
}
// Format the result
return formatter.Format(data)
}
// extractActorURLFromObjectURL extracts the actor URL from an object URL
func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error) {
// This is a simplified approach - in a real app, we would parse the object URL properly
// For now, we'll assume the actor URL is the base domain with the username
// Basic URL pattern: https://domain.tld/@username/postid
parts := strings.Split(objectURL, "/")
if len(parts) < 4 {
return "", fmt.Errorf("invalid object URL format: %s", objectURL)
}
// Extract domain and username
domain := parts[2]
username := parts[3]
// Handle different URL formats
if strings.HasPrefix(username, "@") {
// Format: https://domain.tld/@username/postid
username = strings.TrimPrefix(username, "@")
// Check for cross-instance handles like @user@domain.tld
if strings.Contains(username, "@") {
userParts := strings.Split(username, "@")
if len(userParts) == 2 {
username = userParts[0]
domain = userParts[1]
}
}
// Try common URL patterns
actorURLs := []string{
fmt.Sprintf("https://%s/users/%s", domain, username),
fmt.Sprintf("https://%s/@%s", domain, username),
fmt.Sprintf("https://%s/user/%s", domain, username),
fmt.Sprintf("https://%s/accounts/%s", domain, username),
fmt.Sprintf("https://%s/profile/%s", domain, username),
}
// Try each URL pattern
for _, actorURL := range actorURLs {
fmt.Printf("Trying potential actor URL: %s\n", actorURL)
// Check if this URL returns a valid actor
actorData, err := r.fetchActorData(actorURL)
if err == nil && actorData != nil {
return actorURL, nil
}
// Add a small delay between requests to avoid rate limiting
fmt.Println("Waiting 1 second before trying next actor URL...")
time.Sleep(1 * time.Second)
}
// If we couldn't find a valid actor URL, try WebFinger
fmt.Printf("Trying WebFinger resolution for: %s@%s\n", username, domain)
return r.resolveActorViaWebFinger(username, domain)
} else if username == "users" || username == "user" || username == "accounts" || username == "profile" {
// Format: https://domain.tld/users/username/postid
if len(parts) < 5 {
return "", fmt.Errorf("invalid user URL format: %s", objectURL)
}
actorURL := fmt.Sprintf("https://%s/%s/%s", domain, username, parts[4])
return actorURL, nil
}
// If we get here, we couldn't determine the actor URL
return "", fmt.Errorf("could not determine actor URL from: %s", objectURL)
return bodyBytes, body, nil
}
// fetchActorData fetches actor data from an actor URL
func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, error) {
func (r *Resolver) fetchActorData(actorURL string) ([]byte, map[string]interface{}, error) {
fmt.Printf("Fetching actor data from: %s\n", actorURL)
// Create the request
req, err := http.NewRequest("GET", actorURL, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
return nil, nil, fmt.Errorf("error creating request: %v", err)
}
// Set headers
@ -281,27 +199,27 @@ func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, erro
// Send the request
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetching actor data: %v", err)
return nil, nil, fmt.Errorf("error fetching actor data: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("actor request failed with status: %s", resp.Status)
return nil, nil, fmt.Errorf("actor request failed with status: %s", resp.Status)
}
// Read and parse the response
body, err := io.ReadAll(resp.Body)
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading actor response: %v", err)
return nil, nil, fmt.Errorf("error reading actor response: %v", err)
}
// Parse JSON
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return nil, fmt.Errorf("error parsing actor data: %v", err)
if err := json.Unmarshal(bodyBytes, &data); err != nil {
return nil, nil, fmt.Errorf("error parsing actor data: %v", err)
}
return data, nil
return bodyBytes, data, nil
}
// extractPublicKey extracts the public key ID from actor data
@ -490,14 +408,6 @@ func (r *Resolver) fetchNodeInfo(domain string) ([]byte, map[string]interface{},
// Try to extract actor, else try nodeinfo fallback for top-level domains
func (r *Resolver) ResolveObjectOrNodeInfo(objectURL string) ([]byte, map[string]interface{}, string, error) {
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
if err == nil && actorURL != "" {
actorData, err := r.fetchActorData(actorURL)
if err == nil && actorData != nil {
jsonData, _ := json.MarshalIndent(actorData, "", " ")
return jsonData, actorData, "actor", nil
}
}
// If actor resolution fails, try nodeinfo
parts := strings.Split(objectURL, "/")
if len(parts) < 3 {
@ -516,7 +426,17 @@ func FormatHelperResult(raw []byte, nodeinfo map[string]interface{}) (string, er
return formatter.Format(nodeinfo)
}
// formatCanonicalResultHelper is used by resolver.go to format ActivityPub objects without importing formatter there
func formatCanonicalResultHelper(jsonData []byte, data map[string]interface{}) (string, error) {
return formatter.Format(data)
// Try to always format (ideally the body data, or in the worse case the raw data to string)
func formatResult(raw []byte, data map[string]interface{}) string {
formatted, err := formatter.Format(data)
if err != nil {
return string(raw)
}
return formatted
}
// removeANSIEscapeCodes strips ANSI escape codes from a byte slice
func removeANSIEscapeCodes(input []byte) []byte {
ansiEscape := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
return ansiEscape.ReplaceAll(input, []byte{})
}

View file

@ -45,10 +45,7 @@ func (r *Resolver) Resolve(input string) (string, error) {
if err != nil {
return "", err
}
formatted, ferr := r.formatCanonicalResultHelper(raw, nodeinfo)
if ferr != nil {
return string(raw), nil
}
formatted := formatResult(raw, nodeinfo)
return formatted, nil
}
@ -198,10 +195,10 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
// that differs from the requested URL, recursively fetches that canonical URL. Max depth is used to prevent infinite loops.
func (r *Resolver) resolveCanonicalActivityPub(objectURL string, depth int) (string, error) {
if depth > 3 {
return "", fmt.Errorf("Too many canonical redirects (possible loop)")
return "", fmt.Errorf("too many canonical redirects (possible loop)")
}
fmt.Printf("Fetching ActivityPub object for canonical resolution: %s\n", objectURL)
jsonData, err := r.fetchActivityPubObjectWithSignatureRaw(objectURL)
jsonData, err := r.fetchActivityPubObjectRaw(objectURL)
if err != nil {
return "", err
}
@ -215,48 +212,29 @@ func (r *Resolver) resolveCanonicalActivityPub(objectURL string, depth int) (str
return r.resolveCanonicalActivityPub(idVal, depth+1)
}
// If no id or already canonical, format and return using helpers.go
return r.formatCanonicalResultHelper(jsonData, data)
formatted := formatResult(jsonData, data)
return formatted, nil
}
// fetchActivityPubObjectWithSignatureRaw fetches an ActivityPub object and returns the raw JSON []byte (not formatted)
func (r *Resolver) fetchActivityPubObjectWithSignatureRaw(objectURL string) ([]byte, error) {
fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL)
// fetchActivityPubObjectRaw fetches an ActivityPub object and returns the raw JSON []byte (not formatted)
func (r *Resolver) fetchActivityPubObjectRaw(objectURL string) ([]byte, error) {
fmt.Printf("Fetching ActivityPub object with HTTP from: %s\n", objectURL)
actorURL, err := r.extractActorURLFromObjectURL(objectURL)
if err != nil {
return nil, fmt.Errorf("Could not extract actor URL: %v", err)
}
actorData, err := r.fetchActorData(actorURL)
if err != nil {
return nil, fmt.Errorf("Could not fetch actor data: %v", err)
}
keyID, _, err := r.extractPublicKey(actorData)
if err != nil {
return nil, fmt.Errorf("Could not extract public key: %v", err)
}
privateKey, err := generateRSAKey()
if err != nil {
return nil, fmt.Errorf("Could not generate RSA key: %v", err)
}
// Sign and send the request
req, err := http.NewRequest("GET", objectURL, nil)
if err != nil {
return nil, fmt.Errorf("error creating signed request: %v", err)
return nil, fmt.Errorf("error creating get request: %v", err)
}
req.Header.Set("Accept", "application/ld+json, application/activity+json")
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
if err := signRequest(req, keyID, privateKey); err != nil {
return nil, fmt.Errorf("Could not sign request: %v", err)
}
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending signed request: %v", err)
return nil, fmt.Errorf("error sending get request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
return nil, fmt.Errorf("get request failed with status: %s, body: %s", resp.Status, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
@ -268,12 +246,6 @@ func (r *Resolver) fetchActivityPubObjectWithSignatureRaw(objectURL string) ([]b
return body, nil
}
// formatCanonicalResultHelper formats the ActivityPub object for display
func (r *Resolver) formatCanonicalResultHelper(jsonData []byte, data map[string]interface{}) (string, error) {
// Call the new helper method from helpers.go
return formatCanonicalResultHelper(jsonData, data)
}
// fetchActivityPubObject fetches an ActivityPub object from a URL
// This function now uses a signature-first approach by default
func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) {
@ -291,5 +263,10 @@ func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) {
}
// Use our signature-first approach by default
return r.fetchActivityPubObjectWithSignature(objectURL)
raw, body, err := r.fetchActivityPubObjectWithSignature(objectURL)
if err != nil {
return "", fmt.Errorf("error fetching ActivityPub object: %v", err)
}
formatted := formatResult(raw, body)
return formatted, nil
}