Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(webconnectivitylte): handle too many redirects #1550

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func analysisEngineClassic(tk *TestKeys, logger model.Logger) {
tk.analysisClassic(model.GeoIPASNLookupperFunc(geoipx.LookupASN), logger)
}

func (tk *TestKeys) analysisClassic(lookupper model.GeoIPASNLookupper, logger model.Logger) {
func (tk *TestKeys) analysisClassic(lookupper model.GeoIPASNLookupper, _ model.Logger) {
// Since we run after all tasks have completed (or so we assume) we're
// not going to use any form of locking here.

Expand Down
8 changes: 4 additions & 4 deletions internal/experiment/webconnectivitylte/cleartextflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ func (t *CleartextFlow) httpTransaction(ctx context.Context, network, address, a
}
if err == nil && httpRedirectIsRedirect(resp) {
err = httpValidateRedirect(resp)
if err == nil && t.FollowRedirects && !t.NumRedirects.CanFollowOneMoreRedirect() {
err = ErrTooManyRedirects
}
}

finished := trace.TimeSince(trace.ZeroTime())
Expand Down Expand Up @@ -319,10 +322,7 @@ func (t *CleartextFlow) httpTransaction(ctx context.Context, network, address, a

// maybeFollowRedirects follows redirects if configured and needed
func (t *CleartextFlow) maybeFollowRedirects(ctx context.Context, resp *http.Response) {
if !t.FollowRedirects || !t.NumRedirects.CanFollowOneMoreRedirect() {
return // not configured or too many redirects
}
if httpRedirectIsRedirect(resp) {
if t.FollowRedirects && httpRedirectIsRedirect(resp) {
location, err := resp.Location()
if err != nil {
return // broken response from server
Expand Down
2 changes: 1 addition & 1 deletion internal/experiment/webconnectivitylte/measurer.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error {
Domain: URL.Hostname(),
IDGenerator: NewIDGenerator(),
Logger: sess.Logger(),
NumRedirects: NewNumRedirects(5),
NumRedirects: NewNumRedirects(10),
TestKeys: tk,
URL: URL,
ZeroTime: measurement.MeasurementStartTimeSaved,
Expand Down
2 changes: 1 addition & 1 deletion internal/experiment/webconnectivitylte/redirects.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ func NewNumRedirects(n int64) *NumRedirects {
// CanFollowOneMoreRedirect returns true if we are
// allowed to follow one more redirect.
func (nr *NumRedirects) CanFollowOneMoreRedirect() bool {
return nr.count.Add(-1) > 0
return nr.count.Add(-1) >= 0
}
16 changes: 16 additions & 0 deletions internal/experiment/webconnectivitylte/redirects_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package webconnectivitylte

import "testing"

func TestNumRedirects(t *testing.T) {
const count = 10
nr := NewNumRedirects(count)
for idx := 0; idx < count; idx++ {
if !nr.CanFollowOneMoreRedirect() {
t.Fatal("got false with idx=", idx)
}
}
if nr.CanFollowOneMoreRedirect() {
t.Fatal("got true after the loop")
}
}
12 changes: 8 additions & 4 deletions internal/experiment/webconnectivitylte/secureflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package webconnectivitylte
import (
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -305,6 +306,9 @@ func (t *SecureFlow) newHTTPRequest(ctx context.Context) (*http.Request, error)
return httpReq, nil
}

// ErrTooManyRedirects indicates we have seen too many HTTP redirects.
var ErrTooManyRedirects = errors.New("stopped after too many redirects")

// httpTransaction runs the HTTP transaction and saves the results.
func (t *SecureFlow) httpTransaction(ctx context.Context, network, address, alpn string,
txp model.HTTPTransport, req *http.Request, trace *measurexlite.Trace) (*http.Response, []byte, error) {
Expand Down Expand Up @@ -337,6 +341,9 @@ func (t *SecureFlow) httpTransaction(ctx context.Context, network, address, alpn
}
if err == nil && httpRedirectIsRedirect(resp) {
err = httpValidateRedirect(resp)
if err == nil && t.FollowRedirects && !t.NumRedirects.CanFollowOneMoreRedirect() {
err = ErrTooManyRedirects
}
}

finished := trace.TimeSince(trace.ZeroTime())
Expand Down Expand Up @@ -374,10 +381,7 @@ func (t *SecureFlow) httpTransaction(ctx context.Context, network, address, alpn

// maybeFollowRedirects follows redirects if configured and needed
func (t *SecureFlow) maybeFollowRedirects(ctx context.Context, resp *http.Response) {
if !t.FollowRedirects || !t.NumRedirects.CanFollowOneMoreRedirect() {
return // not configured or too many redirects
}
if httpRedirectIsRedirect(resp) {
if t.FollowRedirects && httpRedirectIsRedirect(resp) {
location, err := resp.Location()
if err != nil {
return // broken response from server
Expand Down
19 changes: 18 additions & 1 deletion internal/netemx/httpbin.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package netemx

import (
"fmt"
"net"
"net/http"
"strconv"
"strings"

"github.com/ooni/netem"
)
Expand All @@ -29,7 +32,7 @@ func HTTPBinHandlerFactory() HTTPHandlerFactory {
// Any other request URL causes a 404 respose.
func HTTPBinHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Date", "Thu, 24 Aug 2023 14:35:29 GMT")
w.Header().Set("Date", "Thu, 24 Aug 2023 14:35:29 GMT")

// missing address => 500
address, _, err := net.SplitHostPort(r.RemoteAddr)
Expand All @@ -44,6 +47,20 @@ func HTTPBinHandler() http.Handler {
secureRedirect := r.URL.Path == "/broken-redirect-https"

switch {
// redirect with count
case strings.HasPrefix(r.URL.Path, "/redirect/"):
count, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/redirect/"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if count <= 0 {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Location", fmt.Sprintf("/redirect/%d", count-1))
w.WriteHeader(http.StatusFound)

// broken HTTP redirect for clients
case cleartextRedirect && client:
w.Header().Set("Location", "http://")
Expand Down
82 changes: 82 additions & 0 deletions internal/netemx/httpbin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,88 @@ func TestHTTPBinHandler(t *testing.T) {
}
})

t.Run("/redirect/{n} with invalid number", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "https://", Path: "/redirect/antani"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort("8.8.8.8", "54321"),
}
rr := httptest.NewRecorder()
handler := HTTPBinHandler()
handler.ServeHTTP(rr, req)
result := rr.Result()
if result.StatusCode != http.StatusBadRequest {
t.Fatal("unexpected status code", result.StatusCode)
}
})

t.Run("/redirect/0", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "https://", Path: "/redirect/0"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort("8.8.8.8", "54321"),
}
rr := httptest.NewRecorder()
handler := HTTPBinHandler()
handler.ServeHTTP(rr, req)
result := rr.Result()
if result.StatusCode != http.StatusOK {
t.Fatal("unexpected status code", result.StatusCode)
}
})

t.Run("/redirect/1", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "https://", Path: "/redirect/1"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort("8.8.8.8", "54321"),
}
rr := httptest.NewRecorder()
handler := HTTPBinHandler()
handler.ServeHTTP(rr, req)
result := rr.Result()
if result.StatusCode != http.StatusFound {
t.Fatal("unexpected status code", result.StatusCode)
}
location, err := result.Location()
if err != nil {
t.Fatal(err)
}
if location.Path != "/redirect/0" {
t.Fatal("unexpected location.Path", location.Path)
}
})

t.Run("/redirect/2", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "https://", Path: "/redirect/2"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort("8.8.8.8", "54321"),
}
rr := httptest.NewRecorder()
handler := HTTPBinHandler()
handler.ServeHTTP(rr, req)
result := rr.Result()
if result.StatusCode != http.StatusFound {
t.Fatal("unexpected status code", result.StatusCode)
}
location, err := result.Location()
if err != nil {
t.Fatal(err)
}
if location.Path != "/redirect/1" {
t.Fatal("unexpected location.Path", location.Path)
}
})

t.Run("/broken-redirect-http with client address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/broken-redirect-http"},
Expand Down
44 changes: 44 additions & 0 deletions internal/webconnectivityqa/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,47 @@ func redirectWithBrokenLocationForHTTPS() *TestCase {
},
}
}

// redirectWithMoreThanTenRedirectsAndHTTP is a scenario where the redirect
// consists of more than ten redirects for http:// URLs.
func redirectWithMoreThanTenRedirectsAndHTTP() *TestCase {
return &TestCase{
Name: "redirectWithMoreThanTenRedirectsAndHTTP",
Flags: TestCaseFlagNoV04,
Input: "http://httpbin.com/redirect/15",
LongTest: true,
ExpectErr: false,
ExpectTestKeys: &TestKeys{
DNSExperimentFailure: nil,
DNSConsistency: "consistent",
HTTPExperimentFailure: `unknown_failure: stopped after too many redirects`,
XStatus: 0,
XDNSFlags: 0,
XBlockingFlags: 0,
Accessible: false,
Blocking: false,
},
}
}

// redirectWithMoreThanTenRedirectsAndHTTPS is a scenario where the redirect
// consists of more than ten redirects for https:// URLs.
func redirectWithMoreThanTenRedirectsAndHTTPS() *TestCase {
return &TestCase{
Name: "redirectWithMoreThanTenRedirectsAndHTTPS",
Flags: TestCaseFlagNoV04,
Input: "https://httpbin.com/redirect/15",
LongTest: true,
ExpectErr: false,
ExpectTestKeys: &TestKeys{
DNSExperimentFailure: nil,
DNSConsistency: "consistent",
HTTPExperimentFailure: `unknown_failure: stopped after too many redirects`,
XStatus: 0,
XDNSFlags: 0,
XBlockingFlags: 0,
Accessible: false,
Blocking: false,
},
}
}
2 changes: 2 additions & 0 deletions internal/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func AllTestCases() []*TestCase {
redirectWithConsistentDNSAndThenEOFForHTTPS(),
redirectWithConsistentDNSAndThenTimeoutForHTTP(),
redirectWithConsistentDNSAndThenTimeoutForHTTPS(),
redirectWithMoreThanTenRedirectsAndHTTP(),
redirectWithMoreThanTenRedirectsAndHTTPS(),

successWithHTTP(),
successWithHTTPS(),
Expand Down
Loading