Clean up code

This commit is contained in:
Melroy van den Berg 2025-04-24 18:21:44 +02:00
parent cd2f554e50
commit e3381774b1
No known key found for this signature in database
GPG key ID: 71D11FF23454B9D7
3 changed files with 108 additions and 112 deletions

View file

@ -6,8 +6,8 @@ import (
"os" "os"
"strings" "strings"
"gitlab.melroy.org/melroy/fediresolve/resolver"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gitlab.melroy.org/melroy/fediresolve/resolver"
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@ -48,7 +48,3 @@ The tool supports both direct URLs to posts/comments/threads and Fediverse handl
func Execute() error { func Execute() error {
return rootCmd.Execute() return rootCmd.Execute()
} }
func init() {
// Here you can define flags and configuration settings
}

View file

@ -5,14 +5,14 @@ import (
"crypto/rsa" "crypto/rsa"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"gitlab.melroy.org/melroy/fediresolve/formatter"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"gitlab.melroy.org/melroy/fediresolve/formatter"
) )
// Define common constants // Define common constants
@ -25,7 +25,7 @@ const (
// This is the preferred way to fetch ActivityPub content as many instances require signatures // 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) (string, error) {
fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL) fmt.Printf("Fetching ActivityPub object with HTTP signatures from: %s\n", objectURL)
// First, we need to extract the actor URL from the object URL // First, we need to extract the actor URL from the object URL
actorURL, err := r.extractActorURLFromObjectURL(objectURL) actorURL, err := r.extractActorURLFromObjectURL(objectURL)
if err != nil { if err != nil {
@ -33,7 +33,7 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
fmt.Printf("Could not extract actor URL: %v, falling back to direct request\n", err) fmt.Printf("Could not extract actor URL: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Then, we need to fetch the actor data to get the public key // Then, we need to fetch the actor data to get the public key
actorData, err := r.fetchActorData(actorURL) actorData, err := r.fetchActorData(actorURL)
if err != nil { if err != nil {
@ -41,15 +41,15 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
fmt.Printf("Could not fetch actor data: %v, falling back to direct request\n", err) fmt.Printf("Could not fetch actor data: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Extract the public key ID // Extract the public key ID
keyID, _, err := r.extractPublicKey(actorData) keyID, _, err := r.extractPublicKey(actorData)
if err != nil { if err != nil {
// If we can't extract the public key, fall back to a direct request // 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) fmt.Printf("Could not extract public key: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Create a new private key for signing (in a real app, we would use a persistent key) // Create a new private key for signing (in a real app, we would use a persistent key)
privateKey, err := generateRSAKey() privateKey, err := generateRSAKey()
if err != nil { if err != nil {
@ -57,25 +57,25 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
fmt.Printf("Could not generate RSA key: %v, falling back to direct request\n", err) fmt.Printf("Could not generate RSA key: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Now, sign and send the request // Now, sign and send the request
req, err := http.NewRequest("GET", objectURL, nil) req, err := http.NewRequest("GET", objectURL, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating signed request: %v", err) return "", fmt.Errorf("error creating signed request: %v", err)
} }
// Set headers // Set headers
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\", application/json")
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
// Sign the request // Sign the request
if err := signRequest(req, keyID, privateKey); err != nil { if err := signRequest(req, keyID, privateKey); err != nil {
// If we can't sign the request, fall back to a direct request // 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) fmt.Printf("Could not sign request: %v, falling back to direct request\n", err)
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Send the request // Send the request
fmt.Printf("Sending signed request with headers: %v\n", req.Header) fmt.Printf("Sending signed request with headers: %v\n", req.Header)
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
@ -83,7 +83,7 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
return "", fmt.Errorf("error sending signed request: %v", err) return "", fmt.Errorf("error sending signed request: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
fmt.Printf("Received response with status: %s\n", resp.Status) fmt.Printf("Received response with status: %s\n", resp.Status)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// If the signed request fails, try a direct request as a fallback // If the signed request fails, try a direct request as a fallback
@ -91,32 +91,32 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
fmt.Println("Signed request failed with auth error, trying direct request as fallback") fmt.Println("Signed request failed with auth error, trying direct request as fallback")
return r.fetchActivityPubObjectDirect(objectURL) return r.fetchActivityPubObjectDirect(objectURL)
} }
// Read body for error info // Read body for error info
body, _ := ioutil.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body)) return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
} }
// Read and parse the response // Read and parse the response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading response: %v", err) return "", fmt.Errorf("error reading response: %v", err)
} }
// Debug output // Debug output
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type")) fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
// Check if the response is empty // Check if the response is empty
if len(body) == 0 { if len(body) == 0 {
return "", fmt.Errorf("received empty response body") return "", fmt.Errorf("received empty response body")
} }
// Try to decode the JSON response // Try to decode the JSON response
var data map[string]interface{} var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil { if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("error decoding response: %v", err) return "", fmt.Errorf("error decoding response: %v", err)
} }
// Format the result // Format the result
return formatter.Format(data) return formatter.Format(data)
} }
@ -125,7 +125,7 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string
// This is used as a fallback when signing fails // This is used as a fallback when signing fails
func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error) { func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error) {
fmt.Printf("Fetching ActivityPub object directly from: %s\n", objectURL) fmt.Printf("Fetching ActivityPub object directly from: %s\n", objectURL)
// Create a custom client that doesn't follow redirects automatically // Create a custom client that doesn't follow redirects automatically
// so we can capture the redirect URL // so we can capture the redirect URL
client := &http.Client{ client := &http.Client{
@ -133,7 +133,7 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
// Create the request // Create the request
req, err := http.NewRequest("GET", objectURL, nil) req, err := http.NewRequest("GET", objectURL, nil)
if err != nil { if err != nil {
@ -153,10 +153,10 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
defer resp.Body.Close() defer resp.Body.Close()
fmt.Printf("Received response with status: %s\n", resp.Status) fmt.Printf("Received response with status: %s\n", resp.Status)
// Check if we got a redirect (302, 301, etc.) // Check if we got a redirect (302, 301, etc.)
if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently || if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently ||
resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect { resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect {
// Get the redirect URL from the Location header // Get the redirect URL from the Location header
redirectURL := resp.Header.Get("Location") redirectURL := resp.Header.Get("Location")
if redirectURL != "" { if redirectURL != "" {
@ -165,27 +165,27 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
return r.fetchActivityPubObjectWithSignature(redirectURL) return r.fetchActivityPubObjectWithSignature(redirectURL)
} }
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// Read body for error info // Read body for error info
body, _ := ioutil.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body)) return "", fmt.Errorf("request failed with status: %s, body: %s", resp.Status, string(body))
} }
// Read and parse the response // Read and parse the response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading response: %v", err) return "", fmt.Errorf("error reading response: %v", err)
} }
// Debug output // Debug output
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type")) fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
// Check if the response is empty // Check if the response is empty
if len(body) == 0 { if len(body) == 0 {
return "", fmt.Errorf("received empty response body") return "", fmt.Errorf("received empty response body")
} }
// Try to decode the JSON response // Try to decode the JSON response
var data map[string]interface{} var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil { if err := json.Unmarshal(body, &data); err != nil {
@ -199,47 +199,47 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error
// fetchWithSignature fetches ActivityPub content using HTTP Signatures // fetchWithSignature fetches ActivityPub content using HTTP Signatures
func (r *Resolver) fetchWithSignature(objectURL string) (string, error) { func (r *Resolver) fetchWithSignature(objectURL string) (string, error) {
fmt.Printf("Fetching with HTTP signatures from: %s\n", objectURL) fmt.Printf("Fetching with HTTP signatures from: %s\n", objectURL)
// First, we need to extract the actor URL from the object URL // First, we need to extract the actor URL from the object URL
actorURL, err := r.extractActorURLFromObjectURL(objectURL) actorURL, err := r.extractActorURLFromObjectURL(objectURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error extracting actor URL: %v", err) return "", fmt.Errorf("error extracting actor URL: %v", err)
} }
// Then, we need to fetch the actor data to get the public key // Then, we need to fetch the actor data to get the public key
actorData, err := r.fetchActorData(actorURL) actorData, err := r.fetchActorData(actorURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error fetching actor data: %v", err) return "", fmt.Errorf("error fetching actor data: %v", err)
} }
// Extract the public key ID // Extract the public key ID
keyID, _, err := r.extractPublicKey(actorData) keyID, _, err := r.extractPublicKey(actorData)
if err != nil { if err != nil {
return "", fmt.Errorf("error extracting public key: %v", err) return "", fmt.Errorf("error extracting public key: %v", err)
} }
// Create a new private key for signing (in a real app, we would use a persistent key) // Create a new private key for signing (in a real app, we would use a persistent key)
privateKey, err := generateRSAKey() privateKey, err := generateRSAKey()
if err != nil { if err != nil {
return "", fmt.Errorf("error generating RSA key: %v", err) return "", fmt.Errorf("error generating RSA key: %v", err)
} }
// Now, sign and send the request // Now, sign and send the request
req, err := http.NewRequest("GET", objectURL, nil) req, err := http.NewRequest("GET", objectURL, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating signed request: %v", err) return "", fmt.Errorf("error creating signed request: %v", err)
} }
// Set headers // Set headers
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\", application/json")
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
// Sign the request // Sign the request
if err := signRequest(req, keyID, privateKey); err != nil { if err := signRequest(req, keyID, privateKey); err != nil {
return "", fmt.Errorf("error signing request: %v", err) return "", fmt.Errorf("error signing request: %v", err)
} }
// Send the request // Send the request
fmt.Printf("Sending signed request with headers: %v\n", req.Header) fmt.Printf("Sending signed request with headers: %v\n", req.Header)
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
@ -247,34 +247,34 @@ func (r *Resolver) fetchWithSignature(objectURL string) (string, error) {
return "", fmt.Errorf("error sending signed request: %v", err) return "", fmt.Errorf("error sending signed request: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
fmt.Printf("Received response with status: %s\n", resp.Status) fmt.Printf("Received response with status: %s\n", resp.Status)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// Read body for error info // Read body for error info
body, _ := ioutil.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body)) return "", fmt.Errorf("signed request failed with status: %s, body: %s", resp.Status, string(body))
} }
// Read and parse the response // Read and parse the response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading response: %v", err) return "", fmt.Errorf("error reading response: %v", err)
} }
// Debug output // Debug output
fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type")) fmt.Printf("Response content type: %s\n", resp.Header.Get("Content-Type"))
// Check if the response is empty // Check if the response is empty
if len(body) == 0 { if len(body) == 0 {
return "", fmt.Errorf("received empty response body") return "", fmt.Errorf("received empty response body")
} }
// Try to decode the JSON response // Try to decode the JSON response
var data map[string]interface{} var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil { if err := json.Unmarshal(body, &data); err != nil {
return "", fmt.Errorf("error decoding response: %v", err) return "", fmt.Errorf("error decoding response: %v", err)
} }
// Format the result // Format the result
return formatter.Format(data) return formatter.Format(data)
} }
@ -283,22 +283,22 @@ func (r *Resolver) fetchWithSignature(objectURL string) (string, error) {
func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error) { func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error) {
// This is a simplified approach - in a real app, we would parse the object URL properly // 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 // For now, we'll assume the actor URL is the base domain with the username
// Basic URL pattern: https://domain.tld/@username/postid // Basic URL pattern: https://domain.tld/@username/postid
parts := strings.Split(objectURL, "/") parts := strings.Split(objectURL, "/")
if len(parts) < 4 { if len(parts) < 4 {
return "", fmt.Errorf("invalid object URL format: %s", objectURL) return "", fmt.Errorf("invalid object URL format: %s", objectURL)
} }
// Extract domain and username // Extract domain and username
domain := parts[2] domain := parts[2]
username := parts[3] username := parts[3]
// Handle different URL formats // Handle different URL formats
if strings.HasPrefix(username, "@") { if strings.HasPrefix(username, "@") {
// Format: https://domain.tld/@username/postid // Format: https://domain.tld/@username/postid
username = strings.TrimPrefix(username, "@") username = strings.TrimPrefix(username, "@")
// Check for cross-instance handles like @user@domain.tld // Check for cross-instance handles like @user@domain.tld
if strings.Contains(username, "@") { if strings.Contains(username, "@") {
userParts := strings.Split(username, "@") userParts := strings.Split(username, "@")
@ -307,7 +307,7 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error
domain = userParts[1] domain = userParts[1]
} }
} }
// Try common URL patterns // Try common URL patterns
actorURLs := []string{ actorURLs := []string{
fmt.Sprintf("https://%s/users/%s", domain, username), fmt.Sprintf("https://%s/users/%s", domain, username),
@ -316,7 +316,7 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error
fmt.Sprintf("https://%s/accounts/%s", domain, username), fmt.Sprintf("https://%s/accounts/%s", domain, username),
fmt.Sprintf("https://%s/profile/%s", domain, username), fmt.Sprintf("https://%s/profile/%s", domain, username),
} }
// Try each URL pattern // Try each URL pattern
for _, actorURL := range actorURLs { for _, actorURL := range actorURLs {
fmt.Printf("Trying potential actor URL: %s\n", actorURL) fmt.Printf("Trying potential actor URL: %s\n", actorURL)
@ -325,12 +325,12 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error
if err == nil && actorData != nil { if err == nil && actorData != nil {
return actorURL, nil return actorURL, nil
} }
// Add a small delay between requests to avoid rate limiting // Add a small delay between requests to avoid rate limiting
fmt.Println("Waiting 1 second before trying next actor URL...") fmt.Println("Waiting 1 second before trying next actor URL...")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
// If we couldn't find a valid actor URL, try WebFinger // If we couldn't find a valid actor URL, try WebFinger
fmt.Printf("Trying WebFinger resolution for: %s@%s\n", username, domain) fmt.Printf("Trying WebFinger resolution for: %s@%s\n", username, domain)
return r.resolveActorViaWebFinger(username, domain) return r.resolveActorViaWebFinger(username, domain)
@ -342,7 +342,7 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error
actorURL := fmt.Sprintf("https://%s/%s/%s", domain, username, parts[4]) actorURL := fmt.Sprintf("https://%s/%s/%s", domain, username, parts[4])
return actorURL, nil return actorURL, nil
} }
// If we get here, we couldn't determine the actor URL // If we get here, we couldn't determine the actor URL
return "", fmt.Errorf("could not determine actor URL from: %s", objectURL) return "", fmt.Errorf("could not determine actor URL from: %s", objectURL)
} }
@ -350,40 +350,40 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error
// fetchActorData fetches actor data from an actor URL // fetchActorData fetches actor data from an actor URL
func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, error) { func (r *Resolver) fetchActorData(actorURL string) (map[string]interface{}, error) {
fmt.Printf("Fetching actor data from: %s\n", actorURL) fmt.Printf("Fetching actor data from: %s\n", actorURL)
// Create the request // Create the request
req, err := http.NewRequest("GET", actorURL, nil) req, err := http.NewRequest("GET", actorURL, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating request: %v", err) return nil, fmt.Errorf("error creating request: %v", err)
} }
// Set headers // Set headers
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\", application/json")
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
// Send the request // Send the request
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching actor data: %v", err) return nil, fmt.Errorf("error fetching actor data: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("actor request failed with status: %s", resp.Status) return nil, fmt.Errorf("actor request failed with status: %s", resp.Status)
} }
// Read and parse the response // Read and parse the response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading actor response: %v", err) return nil, fmt.Errorf("error reading actor response: %v", err)
} }
// Parse JSON // Parse JSON
var data map[string]interface{} var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil { if err := json.Unmarshal(body, &data); err != nil {
return nil, fmt.Errorf("error parsing actor data: %v", err) return nil, fmt.Errorf("error parsing actor data: %v", err)
} }
return data, nil return data, nil
} }
@ -394,7 +394,7 @@ func (r *Resolver) extractPublicKey(actorData map[string]interface{}) (string, s
if err != nil { if err != nil {
return "", "", fmt.Errorf("error marshaling actor data: %v", err) return "", "", fmt.Errorf("error marshaling actor data: %v", err)
} }
// Extract key ID // Extract key ID
keyID := gjson.GetBytes(actorJSON, "publicKey.id").String() keyID := gjson.GetBytes(actorJSON, "publicKey.id").String()
if keyID == "" { if keyID == "" {
@ -404,11 +404,11 @@ func (r *Resolver) extractPublicKey(actorData map[string]interface{}) (string, s
if keyID == "" { if keyID == "" {
return "", "", fmt.Errorf("could not find public key ID in actor data") return "", "", fmt.Errorf("could not find public key ID in actor data")
} }
// For future implementation, we might need to parse and use the public key // For future implementation, we might need to parse and use the public key
// But for now, we just return a dummy value since we're focused on signing // But for now, we just return a dummy value since we're focused on signing
dummyPEM := "dummy-key" dummyPEM := "dummy-key"
return keyID, dummyPEM, nil return keyID, dummyPEM, nil
} }
@ -425,13 +425,13 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er
if req.Header.Get("Host") == "" { if req.Header.Get("Host") == "" {
req.Header.Set("Host", req.URL.Host) req.Header.Set("Host", req.URL.Host)
} }
// For GET requests with no body, we need to handle the digest differently // For GET requests with no body, we need to handle the digest differently
if req.Body == nil { if req.Body == nil {
// Create an empty digest // Create an empty digest
req.Header.Set("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") req.Header.Set("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
} }
// Create a new signer with required headers for ActivityPub // Create a new signer with required headers for ActivityPub
signer, _, err := httpsig.NewSigner( signer, _, err := httpsig.NewSigner(
[]httpsig.Algorithm{httpsig.RSA_SHA256}, []httpsig.Algorithm{httpsig.RSA_SHA256},
@ -443,7 +443,7 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er
if err != nil { if err != nil {
return fmt.Errorf("error creating signer: %v", err) return fmt.Errorf("error creating signer: %v", err)
} }
// Sign the request // Sign the request
return signer.SignRequest(privateKey, keyID, req, nil) return signer.SignRequest(privateKey, keyID, req, nil)
} }
@ -451,38 +451,38 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er
// resolveActorViaWebFinger resolves an actor URL via WebFinger protocol // resolveActorViaWebFinger resolves an actor URL via WebFinger protocol
func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, error) { func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, error) {
// WebFinger URL format: https://domain.tld/.well-known/webfinger?resource=acct:username@domain.tld // WebFinger URL format: https://domain.tld/.well-known/webfinger?resource=acct:username@domain.tld
webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s",
domain, username, domain) domain, username, domain)
fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL) fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL)
// Create the request // Create the request
req, err := http.NewRequest("GET", webfingerURL, nil) req, err := http.NewRequest("GET", webfingerURL, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating WebFinger request: %v", err) return "", fmt.Errorf("error creating WebFinger request: %v", err)
} }
// Set headers // Set headers
req.Header.Set("Accept", "application/jrd+json, application/json") req.Header.Set("Accept", "application/jrd+json, application/json")
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
// Send the request // Send the request
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("error fetching WebFinger data: %v", err) return "", fmt.Errorf("error fetching WebFinger data: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("WebFinger request failed with status: %s", resp.Status) return "", fmt.Errorf("WebFinger request failed with status: %s", resp.Status)
} }
// Read and parse the response // Read and parse the response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading WebFinger response: %v", err) return "", fmt.Errorf("error reading WebFinger response: %v", err)
} }
// Find the ActivityPub actor URL in the WebFinger response // Find the ActivityPub actor URL in the WebFinger response
actorURL := "" actorURL := ""
webfingerData := gjson.ParseBytes(body) webfingerData := gjson.ParseBytes(body)
@ -491,18 +491,18 @@ func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, er
rel := link.Get("rel").String() rel := link.Get("rel").String()
typ := link.Get("type").String() typ := link.Get("type").String()
href := link.Get("href").String() href := link.Get("href").String()
if rel == "self" && (typ == "application/activity+json" || if rel == "self" && (typ == "application/activity+json" ||
typ == "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" || typ == "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ||
strings.Contains(typ, "activity+json")) { strings.Contains(typ, "activity+json")) {
actorURL = href actorURL = href
break break
} }
} }
if actorURL == "" { if actorURL == "" {
return "", fmt.Errorf("could not find ActivityPub actor URL in WebFinger response") return "", fmt.Errorf("could not find ActivityPub actor URL in WebFinger response")
} }
return actorURL, nil return actorURL, nil
} }

View file

@ -3,7 +3,7 @@ package resolver
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -37,7 +37,7 @@ func (r *Resolver) Resolve(input string) (string, error) {
fmt.Println("Detected URL, attempting direct resolution") fmt.Println("Detected URL, attempting direct resolution")
return r.resolveURL(input) return r.resolveURL(input)
} }
// Check if input looks like a Fediverse handle (@username@domain.tld) // Check if input looks like a Fediverse handle (@username@domain.tld)
if strings.Contains(input, "@") { if strings.Contains(input, "@") {
// Handle format should be either @username@domain.tld or username@domain.tld // Handle format should be either @username@domain.tld or username@domain.tld
@ -88,24 +88,24 @@ func (r *Resolver) resolveHandle(handle string) (string, error) {
} }
username, domain := parts[0], parts[1] username, domain := parts[0], parts[1]
// Construct WebFinger URL with proper URL encoding // Construct WebFinger URL with proper URL encoding
resource := fmt.Sprintf("acct:%s@%s", username, domain) resource := fmt.Sprintf("acct:%s@%s", username, domain)
webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=%s", webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=%s",
domain, url.QueryEscape(resource)) domain, url.QueryEscape(resource))
fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL) fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL)
// Create request for WebFinger data // Create request for WebFinger data
req, err := http.NewRequest("GET", webfingerURL, nil) req, err := http.NewRequest("GET", webfingerURL, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating WebFinger request: %v", err) return "", fmt.Errorf("error creating WebFinger request: %v", err)
} }
// Set appropriate headers for WebFinger // Set appropriate headers for WebFinger
req.Header.Set("Accept", "application/jrd+json, application/json") req.Header.Set("Accept", "application/jrd+json, application/json")
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
// Fetch WebFinger data // Fetch WebFinger data
resp, err := r.client.Do(req) resp, err := r.client.Do(req)
if err != nil { if err != nil {
@ -118,14 +118,14 @@ func (r *Resolver) resolveHandle(handle string) (string, error) {
} }
// Read and parse the WebFinger response // Read and parse the WebFinger response
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("error reading WebFinger response: %v", err) return "", fmt.Errorf("error reading WebFinger response: %v", err)
} }
fmt.Printf("WebFinger response content type: %s\n", resp.Header.Get("Content-Type")) fmt.Printf("WebFinger response content type: %s\n", resp.Header.Get("Content-Type"))
fmt.Printf("WebFinger response body: %s\n", string(body)) fmt.Printf("WebFinger response body: %s\n", string(body))
var webfinger WebFingerResponse var webfinger WebFingerResponse
if err := json.Unmarshal(body, &webfinger); err != nil { if err := json.Unmarshal(body, &webfinger); err != nil {
return "", fmt.Errorf("error decoding WebFinger response: %v", err) return "", fmt.Errorf("error decoding WebFinger response: %v", err)
@ -195,7 +195,7 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
if strings.HasPrefix(username, "@") && strings.Contains(username[1:], "@") { if strings.HasPrefix(username, "@") && strings.Contains(username[1:], "@") {
// This is a cross-instance URL // This is a cross-instance URL
fmt.Println("Detected cross-instance URL. Original instance:", strings.Split(username[1:], "/")[0]) fmt.Println("Detected cross-instance URL. Original instance:", strings.Split(username[1:], "/")[0])
// Extract the original instance, username, and post ID // Extract the original instance, username, and post ID
parts := strings.Split(username, "/") parts := strings.Split(username, "/")
if len(parts) >= 2 { if len(parts) >= 2 {
@ -204,10 +204,10 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
username := userParts[0] username := userParts[0]
originalDomain := userParts[1] originalDomain := userParts[1]
postID := parts[1] postID := parts[1]
fmt.Printf("Detected cross-instance URL. Original instance: %s, username: %s, post ID: %s\n", fmt.Printf("Detected cross-instance URL. Original instance: %s, username: %s, post ID: %s\n",
originalDomain, username, postID) originalDomain, username, postID)
// Try different URL formats that are commonly used by different Fediverse platforms // Try different URL formats that are commonly used by different Fediverse platforms
urlFormats := []string{ urlFormats := []string{
// Mastodon format // Mastodon format
@ -222,7 +222,7 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
// Hubzilla format // Hubzilla format
"https://%s/item/%s", "https://%s/item/%s",
} }
// Try each URL format // Try each URL format
for _, format := range urlFormats { for _, format := range urlFormats {
var targetURL string var targetURL string
@ -233,22 +233,22 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
// Format without username (just domain and ID) // Format without username (just domain and ID)
targetURL = fmt.Sprintf(format, originalDomain, postID) targetURL = fmt.Sprintf(format, originalDomain, postID)
} }
fmt.Printf("Trying URL format: %s\n", targetURL) fmt.Printf("Trying URL format: %s\n", targetURL)
// Try to fetch with our signature-first approach // Try to fetch with our signature-first approach
result, err := r.fetchActivityPubObject(targetURL) result, err := r.fetchActivityPubObject(targetURL)
if err == nil { if err == nil {
return result, nil return result, nil
} }
fmt.Printf("Failed with error: %v\n", err) fmt.Printf("Failed with error: %v\n", err)
// Add a delay between requests to avoid rate limiting // Add a delay between requests to avoid rate limiting
fmt.Println("Waiting 2 seconds before trying next URL format...") fmt.Println("Waiting 2 seconds before trying next URL format...")
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
// If all formats fail, return the last error // If all formats fail, return the last error
return "", fmt.Errorf("failed to fetch content from original instance %s: all URL formats tried", originalDomain) return "", fmt.Errorf("failed to fetch content from original instance %s: all URL formats tried", originalDomain)
} }
@ -263,18 +263,18 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
// This function now uses a signature-first approach by default // This function now uses a signature-first approach by default
func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) { func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) {
fmt.Printf("Fetching ActivityPub object from: %s\n", objectURL) fmt.Printf("Fetching ActivityPub object from: %s\n", objectURL)
// Make sure the URL is valid // Make sure the URL is valid
parsedURL, err := url.Parse(objectURL) parsedURL, err := url.Parse(objectURL)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid URL: %v", err) return "", fmt.Errorf("invalid URL: %v", err)
} }
// Ensure the URL has a scheme // Ensure the URL has a scheme
if parsedURL.Scheme == "" { if parsedURL.Scheme == "" {
objectURL = "https://" + objectURL objectURL = "https://" + objectURL
} }
// Use our signature-first approach by default // Use our signature-first approach by default
return r.fetchActivityPubObjectWithSignature(objectURL) return r.fetchActivityPubObjectWithSignature(objectURL)
} }