Try to implement HTTP Signure

This commit is contained in:
Dennis C. Oosterhof 2025-04-24 14:02:37 +02:00
parent d71faa3dd5
commit dc32928e84
3 changed files with 74 additions and 81 deletions

2
go.mod
View file

@ -9,11 +9,13 @@ require (
) )
require ( require (
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.18.0 // indirect
) )

9
go.sum
View file

@ -1,6 +1,8 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -20,9 +22,16 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -8,9 +8,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/dennis/fediresolve/formatter"
"github.com/tidwall/gjson"
) )
// Resolver handles the resolution of Fediverse URLs and handles // Resolver handles the resolution of Fediverse URLs and handles
@ -181,23 +178,75 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) {
// Parse the URL // Parse the URL
parsedURL, err := url.Parse(inputURL) parsedURL, err := url.Parse(inputURL)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid URL: %v", err) return "", fmt.Errorf("error parsing URL: %v", err)
} }
// Ensure the URL has a scheme // Check if this is a cross-instance URL (e.g., https://mastodon.social/@user@another.instance/123)
if parsedURL.Scheme == "" { username := parsedURL.Path
inputURL = "https://" + inputURL if len(username) > 0 && username[0] == '/' {
parsedURL, err = url.Parse(inputURL) username = username[1:]
if err != nil { }
return "", fmt.Errorf("invalid URL: %v", err)
// Check if the username contains an @ symbol (indicating a cross-instance URL)
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 {
userParts := strings.Split(parts[0][1:], "@") // Remove the leading @ and split by @
if len(userParts) == 2 {
username := userParts[0]
originalDomain := userParts[1]
postID := parts[1]
fmt.Printf("Detected cross-instance URL. Original instance: %s, username: %s, post ID: %s\n",
originalDomain, username, postID)
// Try different URL formats that might work for the original instance
formats := []string{
"https://%s/@%s/%s",
"https://%s/users/%s/statuses/%s",
"https://%s/notes/%s",
"https://%s/notice/%s",
}
for _, format := range formats {
originalURL := fmt.Sprintf(format, originalDomain, username, postID)
fmt.Printf("Attempting to fetch from original instance: %s\n", originalURL)
// Try to fetch directly first
fmt.Printf("Trying with ActivityPub direct fetch: %s\n", originalURL)
result, err := r.fetchActivityPubObject(originalURL)
if err == nil {
return result, nil
}
// If direct fetch fails and it's an auth error, try with HTTP signatures
if strings.Contains(err.Error(), "401 Unauthorized") || strings.Contains(err.Error(), "403 Forbidden") {
fmt.Printf("Direct fetch failed with auth error, trying with HTTP signatures: %s\n", originalURL)
result, sigErr := r.fetchWithSignature(originalURL)
if sigErr == nil {
return result, nil
}
fmt.Printf("HTTP signatures fetch also failed: %v\n", sigErr)
}
// If this fails, continue trying other formats
}
// If all formats fail, return the last error
return "", fmt.Errorf("failed to fetch content from original instance %s: all URL formats tried", originalDomain)
}
} }
} }
// Try to fetch the ActivityPub object directly // If not a cross-instance URL, fetch the ActivityPub object directly
return r.fetchActivityPubObject(inputURL) return r.fetchActivityPubObject(inputURL)
} }
// fetchActivityPubObject fetches an ActivityPub object from a URL // 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) { 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)
@ -206,79 +255,12 @@ func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) {
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
parsedURL, err = url.Parse(objectURL)
if err != nil {
return "", fmt.Errorf("invalid URL: %v", err)
}
} }
// Create the request // Use our signature-first approach by default
req, err := http.NewRequest("GET", objectURL, nil) return r.fetchActivityPubObjectWithSignature(objectURL)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Set Accept headers to request ActivityPub data
// Use multiple Accept headers to increase compatibility with different servers
req.Header.Set("Accept", "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json")
req.Header.Set("User-Agent", "FediResolve/1.0 (https://github.com/dennis/fediresolve)")
// Perform the request
fmt.Printf("Sending request with headers: %v\n", req.Header)
resp, err := r.client.Do(req)
if err != nil {
return "", fmt.Errorf("error fetching ActivityPub data: %v", err)
}
defer resp.Body.Close()
fmt.Printf("Received response with status: %s\n", resp.Status)
if resp.StatusCode != http.StatusOK {
// Try to read the error response body for debugging
errorBody, _ := ioutil.ReadAll(resp.Body)
return "", fmt.Errorf("ActivityPub request failed with status: %s\nResponse body: %s",
resp.Status, string(errorBody))
}
// Read and parse the ActivityPub response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading ActivityPub response: %v", err)
}
// Debug output
fmt.Printf("ActivityPub 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 {
// If we can't parse as JSON, return the raw response for debugging
return "", fmt.Errorf("error decoding ActivityPub response: %v\nResponse body: %s",
err, string(body))
}
// Check if this is a shared/forwarded object and we need to fetch the original
jsonData, _ := json.Marshal(data)
jsonStr := string(jsonData)
// Check for various ActivityPub types that might reference an original object
if gjson.Get(jsonStr, "type").String() == "Announce" {
// This is a boost/share, get the original object
originalURL := gjson.Get(jsonStr, "object").String()
if originalURL != "" && (strings.HasPrefix(originalURL, "http://") || strings.HasPrefix(originalURL, "https://")) {
fmt.Printf("Found Announce, following original at: %s\n", originalURL)
return r.fetchActivityPubObject(originalURL)
}
}
// Format the result
return formatter.Format(data)
} }