From e3381774b10a10dd935468bf135f482aef5a3390 Mon Sep 17 00:00:00 2001 From: Melroy van den Berg Date: Thu, 24 Apr 2025 18:21:44 +0200 Subject: [PATCH] Clean up code --- cmd/root.go | 6 +- resolver/helpers.go | 168 +++++++++++++++++++++---------------------- resolver/resolver.go | 46 ++++++------ 3 files changed, 108 insertions(+), 112 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index ad9b314..331eb35 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,8 +6,8 @@ import ( "os" "strings" - "gitlab.melroy.org/melroy/fediresolve/resolver" "github.com/spf13/cobra" + "gitlab.melroy.org/melroy/fediresolve/resolver" ) var rootCmd = &cobra.Command{ @@ -48,7 +48,3 @@ The tool supports both direct URLs to posts/comments/threads and Fediverse handl func Execute() error { return rootCmd.Execute() } - -func init() { - // Here you can define flags and configuration settings -} diff --git a/resolver/helpers.go b/resolver/helpers.go index 352791f..613f90c 100644 --- a/resolver/helpers.go +++ b/resolver/helpers.go @@ -5,14 +5,14 @@ import ( "crypto/rsa" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" - "gitlab.melroy.org/melroy/fediresolve/formatter" "github.com/go-fed/httpsig" "github.com/tidwall/gjson" + "gitlab.melroy.org/melroy/fediresolve/formatter" ) // Define common constants @@ -25,7 +25,7 @@ const ( // This is the preferred way to fetch ActivityPub content as many instances require signatures func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string, 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) 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) return r.fetchActivityPubObjectDirect(objectURL) } - + // Then, we need to fetch the actor data to get the public key actorData, err := r.fetchActorData(actorURL) 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) return r.fetchActivityPubObjectDirect(objectURL) } - - // Extract the public key ID + + // 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) } - + // Create a new private key for signing (in a real app, we would use a persistent key) privateKey, err := generateRSAKey() 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) return r.fetchActivityPubObjectDirect(objectURL) } - + // 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) } - + // Set headers 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("Date", time.Now().UTC().Format(http.TimeFormat)) - + // 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) } - + // Send the request fmt.Printf("Sending signed request with headers: %v\n", req.Header) 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) } 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 @@ -91,32 +91,32 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string fmt.Println("Signed request failed with auth error, trying direct request as fallback") return r.fetchActivityPubObjectDirect(objectURL) } - + // 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)) } - + // Read and parse the response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", 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") } - + // 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) } - + // Format the result return formatter.Format(data) } @@ -125,7 +125,7 @@ func (r *Resolver) fetchActivityPubObjectWithSignature(objectURL string) (string // This is used as a fallback when signing fails func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error) { fmt.Printf("Fetching ActivityPub object directly from: %s\n", objectURL) - + // Create a custom client that doesn't follow redirects automatically // so we can capture the redirect URL client := &http.Client{ @@ -133,7 +133,7 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error return http.ErrUseLastResponse }, } - + // Create the request req, err := http.NewRequest("GET", objectURL, nil) if err != nil { @@ -153,10 +153,10 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error defer resp.Body.Close() fmt.Printf("Received response with status: %s\n", resp.Status) - + // Check if we got a redirect (302, 301, etc.) - if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently || - resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect { + if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently || + resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect { // Get the redirect URL from the Location header redirectURL := resp.Header.Get("Location") if redirectURL != "" { @@ -165,27 +165,27 @@ func (r *Resolver) fetchActivityPubObjectDirect(objectURL string) (string, error return r.fetchActivityPubObjectWithSignature(redirectURL) } } - + if resp.StatusCode != http.StatusOK { // 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)) } // Read and parse the response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", 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") } - + // Try to decode the JSON response var data map[string]interface{} 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 func (r *Resolver) fetchWithSignature(objectURL string) (string, error) { fmt.Printf("Fetching with HTTP signatures from: %s\n", objectURL) - + // First, we need to extract the actor URL from the object URL actorURL, err := r.extractActorURLFromObjectURL(objectURL) if err != nil { return "", fmt.Errorf("error extracting actor URL: %v", err) } - + // Then, we need to fetch the actor data to get the public key actorData, err := r.fetchActorData(actorURL) if err != nil { return "", fmt.Errorf("error fetching actor data: %v", err) } - - // Extract the public key ID + + // Extract the public key ID keyID, _, err := r.extractPublicKey(actorData) if err != nil { 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) privateKey, err := generateRSAKey() if err != nil { return "", fmt.Errorf("error generating 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) } - + // Set headers 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("Date", time.Now().UTC().Format(http.TimeFormat)) - + // Sign the request if err := signRequest(req, keyID, privateKey); err != nil { return "", fmt.Errorf("error signing request: %v", err) } - + // Send the request fmt.Printf("Sending signed request with headers: %v\n", req.Header) 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) } defer resp.Body.Close() - + fmt.Printf("Received response with status: %s\n", resp.Status) if resp.StatusCode != http.StatusOK { // 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)) } - + // Read and parse the response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", 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") } - + // 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) } - + // Format the result return formatter.Format(data) } @@ -283,22 +283,22 @@ func (r *Resolver) fetchWithSignature(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 // 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, "@") @@ -307,7 +307,7 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error domain = userParts[1] } } - + // Try common URL patterns actorURLs := []string{ 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/profile/%s", domain, username), } - + // Try each URL pattern for _, actorURL := range actorURLs { 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 { 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) @@ -342,7 +342,7 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error 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) } @@ -350,40 +350,40 @@ func (r *Resolver) extractActorURLFromObjectURL(objectURL string) (string, error // fetchActorData fetches actor data from an actor URL func (r *Resolver) fetchActorData(actorURL string) (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) } - + // Set headers 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) - + // Send the request resp, err := r.client.Do(req) if err != nil { return 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) } - + // Read and parse the response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return 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) } - + return data, nil } @@ -394,7 +394,7 @@ func (r *Resolver) extractPublicKey(actorData map[string]interface{}) (string, s if err != nil { return "", "", fmt.Errorf("error marshaling actor data: %v", err) } - + // Extract key ID keyID := gjson.GetBytes(actorJSON, "publicKey.id").String() if keyID == "" { @@ -404,11 +404,11 @@ func (r *Resolver) extractPublicKey(actorData map[string]interface{}) (string, s if keyID == "" { 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 // But for now, we just return a dummy value since we're focused on signing dummyPEM := "dummy-key" - + return keyID, dummyPEM, nil } @@ -425,13 +425,13 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er if req.Header.Get("Host") == "" { req.Header.Set("Host", req.URL.Host) } - + // For GET requests with no body, we need to handle the digest differently if req.Body == nil { // Create an empty digest req.Header.Set("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") } - + // Create a new signer with required headers for ActivityPub signer, _, err := httpsig.NewSigner( []httpsig.Algorithm{httpsig.RSA_SHA256}, @@ -443,7 +443,7 @@ func signRequest(req *http.Request, keyID string, privateKey *rsa.PrivateKey) er if err != nil { return fmt.Errorf("error creating signer: %v", err) } - + // Sign the request 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 func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, error) { // 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) - + fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL) - + // Create the request req, err := http.NewRequest("GET", webfingerURL, nil) if err != nil { return "", fmt.Errorf("error creating WebFinger request: %v", err) } - + // Set headers req.Header.Set("Accept", "application/jrd+json, application/json") req.Header.Set("User-Agent", UserAgent) - + // Send the request resp, err := r.client.Do(req) if err != nil { return "", fmt.Errorf("error fetching WebFinger data: %v", err) } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("WebFinger request failed with status: %s", resp.Status) } - + // Read and parse the response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("error reading WebFinger response: %v", err) } - + // Find the ActivityPub actor URL in the WebFinger response actorURL := "" webfingerData := gjson.ParseBytes(body) @@ -491,18 +491,18 @@ func (r *Resolver) resolveActorViaWebFinger(username, domain string) (string, er rel := link.Get("rel").String() typ := link.Get("type").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\"" || strings.Contains(typ, "activity+json")) { actorURL = href break } } - + if actorURL == "" { return "", fmt.Errorf("could not find ActivityPub actor URL in WebFinger response") } - + return actorURL, nil } diff --git a/resolver/resolver.go b/resolver/resolver.go index d54e189..ec815f3 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -3,7 +3,7 @@ package resolver import ( "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -37,7 +37,7 @@ func (r *Resolver) Resolve(input string) (string, error) { fmt.Println("Detected URL, attempting direct resolution") return r.resolveURL(input) } - + // Check if input looks like a Fediverse handle (@username@domain.tld) if strings.Contains(input, "@") { // 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] - + // Construct WebFinger URL with proper URL encoding 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)) fmt.Printf("Fetching WebFinger data from: %s\n", webfingerURL) - + // Create request for WebFinger data req, err := http.NewRequest("GET", webfingerURL, nil) if err != nil { return "", fmt.Errorf("error creating WebFinger request: %v", err) } - + // Set appropriate headers for WebFinger req.Header.Set("Accept", "application/jrd+json, application/json") req.Header.Set("User-Agent", UserAgent) - + // Fetch WebFinger data resp, err := r.client.Do(req) if err != nil { @@ -118,14 +118,14 @@ func (r *Resolver) resolveHandle(handle string) (string, error) { } // Read and parse the WebFinger response - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { 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 body: %s\n", string(body)) - + var webfinger WebFingerResponse if err := json.Unmarshal(body, &webfinger); err != nil { 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:], "@") { // This is a cross-instance URL fmt.Println("Detected cross-instance URL. Original instance:", strings.Split(username[1:], "/")[0]) - + // Extract the original instance, username, and post ID parts := strings.Split(username, "/") if len(parts) >= 2 { @@ -204,10 +204,10 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) { username := userParts[0] originalDomain := userParts[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) - + // Try different URL formats that are commonly used by different Fediverse platforms urlFormats := []string{ // Mastodon format @@ -222,7 +222,7 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) { // Hubzilla format "https://%s/item/%s", } - + // Try each URL format for _, format := range urlFormats { var targetURL string @@ -233,22 +233,22 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) { // Format without username (just domain and ID) targetURL = fmt.Sprintf(format, originalDomain, postID) } - + fmt.Printf("Trying URL format: %s\n", targetURL) - + // Try to fetch with our signature-first approach result, err := r.fetchActivityPubObject(targetURL) if err == nil { return result, nil } - + fmt.Printf("Failed with error: %v\n", err) - + // Add a delay between requests to avoid rate limiting fmt.Println("Waiting 2 seconds before trying next URL format...") time.Sleep(2 * time.Second) } - + // If all formats fail, return the last error 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 func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) { fmt.Printf("Fetching ActivityPub object from: %s\n", objectURL) - + // Make sure the URL is valid parsedURL, err := url.Parse(objectURL) if err != nil { return "", fmt.Errorf("invalid URL: %v", err) } - + // Ensure the URL has a scheme if parsedURL.Scheme == "" { objectURL = "https://" + objectURL } - + // Use our signature-first approach by default return r.fetchActivityPubObjectWithSignature(objectURL) }