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

chore(webconnectivityqa): reproduce ooni/probe#2628 issue #1479

Merged
merged 5 commits into from
Jan 31, 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
54 changes: 54 additions & 0 deletions internal/experiment/webconnectivityqa/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,57 @@ func redirectWithConsistentDNSAndThenTimeoutForHTTPS() *TestCase {
},
}
}

// redirectWithBrokenLocationForHTTP is a scenario where the redirect
// returns a broken URL only containing `http://`.
//
// See https://github.com/ooni/probe/issues/2628 for more info.
func redirectWithBrokenLocationForHTTP() *TestCase {
return &TestCase{
Name: "redirectWithBrokenLocationForHTTP",
Flags: TestCaseFlagNoLTE,
Input: "http://httpbin.com/broken-redirect-http",
LongTest: true,
Configure: func(env *netemx.QAEnv) {
// nothing
},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSExperimentFailure: nil,
DNSConsistency: "consistent",
HTTPExperimentFailure: "unknown_failure: http: no Host in request URL",
XStatus: 8192, // StatusExperimentHTTP
XDNSFlags: 0,
XBlockingFlags: 1, // AnalysisBlockingFlagDNSBlocking
Accessible: nil,
Blocking: nil,
},
}
}

// redirectWithBrokenLocationForHTTPS is a scenario where the redirect
// returns a broken URL only containing `https://`.
//
// See https://github.com/ooni/probe/issues/2628 for more info.
func redirectWithBrokenLocationForHTTPS() *TestCase {
return &TestCase{
Name: "redirectWithBrokenLocationForHTTPS",
Flags: TestCaseFlagNoLTE,
Input: "https://httpbin.com/broken-redirect-https",
LongTest: true,
Configure: func(env *netemx.QAEnv) {
// nothing
},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSExperimentFailure: nil,
DNSConsistency: "consistent",
HTTPExperimentFailure: "unknown_failure: http: no Host in request URL",
XStatus: 8192, // StatusExperimentHTTP
XDNSFlags: 0,
XBlockingFlags: 1, // AnalysisBlockingFlagDNSBlocking
Accessible: nil,
Blocking: nil,
},
}
}
2 changes: 2 additions & 0 deletions internal/experiment/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func AllTestCases() []*TestCase {
localhostWithHTTP(),
localhostWithHTTPS(),

redirectWithBrokenLocationForHTTP(),
redirectWithBrokenLocationForHTTPS(),
redirectWithConsistentDNSAndThenConnectionRefusedForHTTP(),
redirectWithConsistentDNSAndThenConnectionRefusedForHTTPS(),
redirectWithConsistentDNSAndThenConnectionResetForHTTP(),
Expand Down
8 changes: 6 additions & 2 deletions internal/netemx/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,9 @@ const AddressYandexCom3 = "77.88.55.77"
// AddressYandexCom4 is the fourth address associated with yandex.com.
const AddressYandexCom4 = "77.88.55.80"

// CloudflareCacheAddress1 is the first address associated with cloudflare caches.
const CloudflareCacheAddress1 = "104.16.132.229"
// AddressCloudflareCache1 is the first address associated with cloudflare caches.
const AddressCloudflareCache1 = "104.16.132.229"

// AddressHTTPBinCom1 is the first address associated an httpbin.com-like
// service which our QA environment exports as httpbin.com.
const AddressHTTPBinCom1 = "172.67.144.64"
70 changes: 70 additions & 0 deletions internal/netemx/httpbin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package netemx

import (
"net"
"net/http"

"github.com/ooni/netem"
)

// HTTPBinHandlerFactory constructs an [HTTPBinHandler].
func HTTPBinHandlerFactory() HTTPHandlerFactory {
return HTTPHandlerFactoryFunc(func(env NetStackServerFactoryEnv, stack *netem.UNetStack) http.Handler {
return HTTPBinHandler()
})
}

// HTTPBinHandler returns the [http.Handler] implementing an httpbin.com-like service.
//
// We currently implement the following API endpoints:
//
// /broken-redirect-http
// When accessed by the OONI Probe client redirects with 302 to http:// and
// otherwise redirects to the https://www.example.com/ URL.
//
// /broken-redirect-https
// When accessed by the OONI Probe client redirects with 302 to https:// and
// otherwise redirects to the https://www.example.com/ URL.
//
// Any other request URL causes a 404 respose.
func HTTPBinHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// missing address => 500
address, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

// compute variables used by the switch below
cleartextRedirect := r.URL.Path == "/broken-redirect-http"
client := address == DefaultClientAddress
secureRedirect := r.URL.Path == "/broken-redirect-https"

switch {
// broken HTTP redirect for clients
case cleartextRedirect && client:
w.Header().Set("Location", "http://")
w.WriteHeader(http.StatusFound)

// working HTTP redirect for anyone else
case cleartextRedirect && !client:
w.Header().Set("Location", "http://www.example.com/")
w.WriteHeader(http.StatusFound)

// broken HTTPS redirect for clients
case secureRedirect && client:
w.Header().Set("Location", "https://")
w.WriteHeader(http.StatusFound)

// working HTTPS redirect for anyone else
case secureRedirect && !client:
w.Header().Set("Location", "https://www.example.com/")
w.WriteHeader(http.StatusFound)

// otherwise
default:
w.WriteHeader(http.StatusNotFound)
}
})
}
124 changes: 124 additions & 0 deletions internal/netemx/httpbin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package netemx

import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)

func TestHTTPBinHandler(t *testing.T) {
t.Run("missing client address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
}
rr := httptest.NewRecorder()
handler := HTTPBinHandler()
handler.ServeHTTP(rr, req)
result := rr.Result()
if result.StatusCode != http.StatusInternalServerError {
t.Fatal("unexpected status code", result.StatusCode)
}
})

t.Run("/broken-redirect-http with client address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/broken-redirect-http"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort(DefaultClientAddress, "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)
}
if loc := result.Header.Get("Location"); loc != "http://" {
t.Fatal("unexpected location", loc)
}
})

t.Run("/broken-redirect-http with another address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/broken-redirect-http"},
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)
}
if loc := result.Header.Get("Location"); loc != "http://www.example.com/" {
t.Fatal("unexpected location", loc)
}
})

t.Run("/broken-redirect-https with client address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/broken-redirect-https"},
Body: http.NoBody,
Close: false,
Host: "httpbin.com",
RemoteAddr: net.JoinHostPort(DefaultClientAddress, "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)
}
if loc := result.Header.Get("Location"); loc != "https://" {
t.Fatal("unexpected location", loc)
}
})

t.Run("/broken-redirect-https with another address", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "http://", Path: "/broken-redirect-https"},
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)
}
if loc := result.Header.Get("Location"); loc != "https://www.example.com/" {
t.Fatal("unexpected location", loc)
}
})

t.Run("/nonexistent URL", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Scheme: "https://", Path: "/nonexistent"},
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.StatusNotFound {
t.Fatal("unexpected status code", result.StatusCode)
}
})
}
13 changes: 12 additions & 1 deletion internal/netemx/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ var InternetScenario = []*ScenarioDomainAddresses{{
WebServerFactory: YandexHandlerFactory(),
}, {
Addresses: []string{
CloudflareCacheAddress1,
AddressCloudflareCache1,
},
Domains: []string{
"www.cloudflare-cache.com",
Expand All @@ -218,6 +218,17 @@ var InternetScenario = []*ScenarioDomainAddresses{{
ServerNameMain: "www.cloudflare-cache.com",
ServerNameExtras: []string{},
WebServerFactory: CloudflareCAPTCHAHandlerFactory(),
}, {
Addresses: []string{
AddressHTTPBinCom1,
},
Domains: []string{
"httpbin.com",
},
Role: ScenarioRoleWebServer,
ServerNameMain: "httpbin.com",
ServerNameExtras: []string{},
WebServerFactory: HTTPBinHandlerFactory(),
}}

// MustNewScenario constructs a complete testing scenario using the domains and IP
Expand Down
2 changes: 1 addition & 1 deletion internal/netemx/yandex.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func YandexHandlerFactory() HTTPHandlerFactory {
})
}

// YandexHandler returns the [http.Handler] for yandex.
// YandexHandler returns the [http.Handler] for yandex.com.
func YandexHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Alt-Svc", `h3=":443"`)
Expand Down
Loading