-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add "terminus" -plugin for resolving redirects (#10)
* feat: add "terminus" -plugin for resolving redirects * ci(lint): use golangci-lint-action v4 * feat: allow disabling individual plugins using "isDisabled" configuration option * feat: make terminus -plugin configurable
- Loading branch information
Showing
11 changed files
with
283 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package mock | ||
|
||
import "net/http" | ||
|
||
type RoundTripper struct { | ||
RoundTripFunc func(r *http.Request) (*http.Response, error) | ||
} | ||
|
||
func (rt *RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { | ||
return rt.RoundTripFunc(r) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/mitchellh/mapstructure" | ||
|
||
"github.com/strobotti/linkquisition" | ||
) | ||
|
||
type TerminusPluginSettings struct { | ||
MaxRedirects int `json:"maxRedirects"` | ||
RequestTimeout string `json:"requestTimeout"` | ||
} | ||
|
||
var _ linkquisition.Plugin = (*terminus)(nil) | ||
|
||
type terminus struct { | ||
MaxRedirects int | ||
Client *http.Client | ||
RequestTimeout time.Duration | ||
serviceProvider linkquisition.PluginServiceProvider | ||
} | ||
|
||
func (p *terminus) Setup(serviceProvider linkquisition.PluginServiceProvider, config map[string]interface{}) { | ||
p.MaxRedirects = 5 | ||
p.RequestTimeout = time.Millisecond * 2000 | ||
|
||
var settings TerminusPluginSettings | ||
if err := mapstructure.Decode(config, &settings); err != nil { | ||
serviceProvider.GetLogger().Warn("error decoding settings", "error", err.Error(), "plugin", "terminus") | ||
} else { | ||
if settings.MaxRedirects > 0 { | ||
p.MaxRedirects = settings.MaxRedirects | ||
} | ||
if settings.RequestTimeout != "" { | ||
if timeout, err := time.ParseDuration(settings.RequestTimeout); err != nil { | ||
serviceProvider.GetLogger().Warn( | ||
"requestTimeout configuration option is malformed", "error", err.Error(), "plugin", | ||
"terminus", | ||
) | ||
} else { | ||
p.RequestTimeout = timeout | ||
} | ||
} | ||
} | ||
|
||
p.serviceProvider = serviceProvider | ||
p.Client = &http.Client{ | ||
CheckRedirect: func(req *http.Request, via []*http.Request) error { | ||
return http.ErrUseLastResponse | ||
}, | ||
} | ||
} | ||
|
||
func (p *terminus) ModifyUrl(url string) string { | ||
newUrl := url | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), p.RequestTimeout) | ||
defer cancel() | ||
|
||
for i := 0; i < p.MaxRedirects; i++ { | ||
req, _ := http.NewRequestWithContext(ctx, http.MethodHead, newUrl, http.NoBody) | ||
req.Header.Set("User-Agent", "linkquisition") | ||
resp, err := p.Client.Do(req) | ||
if err != nil { | ||
p.serviceProvider.GetLogger().Warn(fmt.Sprintf("error requesting HEAD %s", newUrl), "error", err.Error(), "plugin", "terminus") | ||
return newUrl | ||
} | ||
|
||
if resp.Body != nil { | ||
_ = resp.Body.Close() | ||
} | ||
|
||
if resp.StatusCode < 300 || resp.StatusCode >= 400 { | ||
// we got a non-redirect response, so we have reached our final destination | ||
break | ||
} | ||
|
||
location := resp.Header.Get("Location") | ||
|
||
if location == "" { | ||
// for whatever reason the location -header doesn't contain a URL; skip | ||
p.serviceProvider.GetLogger().Warn(fmt.Sprintf("no location-header for HEAD %s", newUrl), "plugin", "terminus") | ||
break | ||
} | ||
|
||
p.serviceProvider.GetLogger().Debug( | ||
fmt.Sprintf("following a redirect for %s", newUrl), "location", location, "plugin", "terminus", | ||
) | ||
|
||
newUrl = location | ||
} | ||
|
||
return newUrl | ||
} | ||
|
||
var Plugin terminus |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package main_test | ||
|
||
import ( | ||
"log/slog" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/strobotti/linkquisition" | ||
|
||
"github.com/strobotti/linkquisition/mock" | ||
. "github.com/strobotti/linkquisition/plugins/terminus" | ||
) | ||
|
||
func TestTerminus_ModifyUrl(t *testing.T) { | ||
mockIoWriter := mock.Writer{ | ||
WriteFunc: func(p []byte) (n int, err error) { | ||
return len(p), nil | ||
}, | ||
} | ||
logger := slog.New(slog.NewTextHandler(mockIoWriter, nil)) | ||
|
||
for _, tt := range []struct { | ||
name string | ||
inputUrl string | ||
expectedUrl string | ||
locations map[string]string | ||
responseCodes map[string]int | ||
}{ | ||
{ | ||
name: "original url should be returned if no redirect is detected", | ||
inputUrl: "https://www.example.com/some/thing?here=again", | ||
expectedUrl: "https://www.example.com/some/thing?here=again", | ||
}, | ||
{ | ||
name: "a url from location -header should be returned if a redirect is detected", | ||
inputUrl: "https://www.example.com/some/thing?here=again", | ||
expectedUrl: "https://www2.example.com/some/thing?here=again", | ||
locations: map[string]string{ | ||
"https://www.example.com/some/thing?here=again": "https://www2.example.com/some/thing?here=again", | ||
}, | ||
responseCodes: map[string]int{ | ||
"https://www.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
}, | ||
}, | ||
{ | ||
name: "a chain of redirects works as expected", | ||
inputUrl: "https://www.example.com/some/thing?here=again", | ||
expectedUrl: "https://www3.example.com/some/thing?here=again", | ||
locations: map[string]string{ | ||
"https://www.example.com/some/thing?here=again": "https://www2.example.com/some/thing?here=again", | ||
"https://www2.example.com/some/thing?here=again": "https://www3.example.com/some/thing?here=again", | ||
}, | ||
responseCodes: map[string]int{ | ||
"https://www.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www2.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
}, | ||
}, | ||
{ | ||
name: "a chain of redirects is capped to 5 hops", | ||
inputUrl: "https://www.example.com/some/thing?here=again", | ||
expectedUrl: "https://www6.example.com/some/thing?here=again", | ||
locations: map[string]string{ | ||
"https://www.example.com/some/thing?here=again": "https://www2.example.com/some/thing?here=again", | ||
"https://www2.example.com/some/thing?here=again": "https://www3.example.com/some/thing?here=again", | ||
"https://www3.example.com/some/thing?here=again": "https://www4.example.com/some/thing?here=again", | ||
"https://www4.example.com/some/thing?here=again": "https://www5.example.com/some/thing?here=again", | ||
"https://www5.example.com/some/thing?here=again": "https://www6.example.com/some/thing?here=again", | ||
"https://www6.example.com/some/thing?here=again": "https://www7.example.com/some/thing?here=again", | ||
"https://www7.example.com/some/thing?here=again": "https://www8.example.com/some/thing?here=again", | ||
}, | ||
responseCodes: map[string]int{ | ||
"https://www.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www2.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www3.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www4.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www5.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www6.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
"https://www7.example.com/some/thing?here=again": http.StatusMultipleChoices, | ||
}, | ||
}, | ||
} { | ||
t.Run( | ||
tt.name, func(t *testing.T) { | ||
testedPlugin := Plugin | ||
|
||
provider := linkquisition.NewPluginServiceProvider(logger, &linkquisition.Settings{}) | ||
testedPlugin.Setup(provider, map[string]interface{}{}) | ||
testedPlugin.Client.Transport = &mock.RoundTripper{ | ||
RoundTripFunc: func(r *http.Request) (*http.Response, error) { | ||
resp := &http.Response{ | ||
StatusCode: http.StatusOK, | ||
Header: http.Header{}, | ||
} | ||
|
||
if code, ok := tt.responseCodes[r.URL.String()]; ok { | ||
resp.StatusCode = code | ||
} | ||
if location, ok := tt.locations[r.URL.String()]; ok { | ||
resp.Header.Set("Location", location) | ||
} | ||
|
||
return resp, nil | ||
}, | ||
} | ||
|
||
assert.Equal(t, tt.expectedUrl, testedPlugin.ModifyUrl(tt.inputUrl)) | ||
}, | ||
) | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters