diff --git a/go.mod b/go.mod index bf2de65..3a0e6d5 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,13 @@ require ( ) require ( + github.com/go-fed/httpsig 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-isatty v0.0.20 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/match v1.1.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 ) diff --git a/go.sum b/go.sum index e05dacc..ed3e77c 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ 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/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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/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/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/resolver/resolver.go b/resolver/resolver.go index 44b74ce..fb4f0b6 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -8,9 +8,6 @@ import ( "net/url" "strings" "time" - - "github.com/dennis/fediresolve/formatter" - "github.com/tidwall/gjson" ) // Resolver handles the resolution of Fediverse URLs and handles @@ -181,23 +178,75 @@ func (r *Resolver) resolveURL(inputURL string) (string, error) { // Parse the URL parsedURL, err := url.Parse(inputURL) if err != nil { - return "", fmt.Errorf("invalid URL: %v", err) + return "", fmt.Errorf("error parsing URL: %v", err) } - // Ensure the URL has a scheme - if parsedURL.Scheme == "" { - inputURL = "https://" + inputURL - parsedURL, err = url.Parse(inputURL) - if err != nil { - return "", fmt.Errorf("invalid URL: %v", err) + // Check if this is a cross-instance URL (e.g., https://mastodon.social/@user@another.instance/123) + username := parsedURL.Path + if len(username) > 0 && username[0] == '/' { + username = username[1:] + } + + // 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) } // 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) { fmt.Printf("Fetching ActivityPub object from: %s\n", objectURL) @@ -206,79 +255,12 @@ func (r *Resolver) fetchActivityPubObject(objectURL string) (string, error) { if err != nil { return "", fmt.Errorf("invalid URL: %v", err) } - + // Ensure the URL has a scheme if parsedURL.Scheme == "" { objectURL = "https://" + objectURL - parsedURL, err = url.Parse(objectURL) - if err != nil { - return "", fmt.Errorf("invalid URL: %v", err) - } } - // Create the request - req, err := http.NewRequest("GET", objectURL, nil) - 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) + // Use our signature-first approach by default + return r.fetchActivityPubObjectWithSignature(objectURL) }