diff --git a/Makefile b/Makefile index fdec29df2..d03289483 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ test-kubo-subdomains: provision-kubo gateway-conformance test-kubo: provision-kubo fixtures.car gateway-conformance ./gateway-conformance test --json output.json --gateway-url http://127.0.0.1:8080 --specs -subdomain-gateway +test-randomizer: provision-kubo fixtures.car gateway-conformance + ./gateway-conformance test --json output.json --gateway-url http://127.0.0.1:4242 --specs -subdomain-gateway + provision-cargateway: ./fixtures.car # cd go-libipfs/examples/car && go install car -c ./fixtures.car & @@ -21,6 +24,9 @@ provision-kubo: fixtures.car: gateway-conformance ./gateway-conformance extract-fixtures --merged=true --dir=. +randomizer: + go build -o ./randomizer ./cmd/randomizer + gateway-conformance: go build -o ./gateway-conformance ./cmd/gateway-conformance @@ -37,4 +43,4 @@ output.html: output.xml docker run --rm -v "${PWD}:/workspace" -w "/workspace" ghcr.io/pl-strflt/saxon:v1 -s:output.xml -xsl:/etc/junit-noframes-saxon.xsl -o:output.html open ./output.html -.PHONY: gateway-conformance +.PHONY: gateway-conformance randomizer \ No newline at end of file diff --git a/cmd/randomizer/main.go b/cmd/randomizer/main.go new file mode 100644 index 000000000..60e35528e --- /dev/null +++ b/cmd/randomizer/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +// list of all headers we want to mess with. +// It's a map from string to bool to make finding a header quick +type Messer func([]string) + +var messedHeaders = map[string]Messer{ + "Content-Type": swapRandomStrings, + "Content-Length": messRandomNumbers, + "Content-Encoding": swapRandomStrings, + "Content-Language": swapRandomStrings, + "Content-Location": swapRandomStrings, + "Content-MD5": swapRandomStrings, + "Content-Range": swapRandomStrings, + "Content-Disposition": swapRandomStrings, + "Content-Features": swapRandomStrings, + "Content-Security-Policy": swapRandomStrings, + "Cache-Control": swapRandomStrings, + "X-Ipfs-Path": swapRandomStrings, + "X-Ipfs-Roots": swapRandomStrings, + "X-Content-Type-Options": swapRandomStrings, + "Etag": swapRandomStrings, + "Location": swapRandomStrings, + "Accept-Ranges": swapRandomStrings, + "If-None-Match": swapRandomStrings, +} + +var addedHeaders = map[string]string{ + "Cache-Control": "no-cache", +} + +func init() { + // go through all the headers and just switch to lowercase keys: + for k, v := range messedHeaders { + kk := strings.ToLower(k) + fmt.Println("Replacing:", k, "with:", kk) + delete(messedHeaders, k) + messedHeaders[kk] = v + } + + // print all kv in messedHeaders + for k, v := range messedHeaders { + fmt.Println("messedHeaders:", k, v) + } +} + +func main() { + // Parse command-line arguments for target URL and proxy address + var targetUrlStr, proxyAddr string + flag.StringVar(&targetUrlStr, "target", "", "Target URL to proxy requests to") + flag.StringVar(&proxyAddr, "proxy", "", "Address to listen for proxy requests") + flag.Parse() + + if targetUrlStr == "" || proxyAddr == "" { + fmt.Println("Usage: randomizer -target -proxy ") + return + } + + // Parse the target URL and create a reverse proxy + targetUrl, err := url.Parse(targetUrlStr) + if err != nil { + panic(err) + } + proxy := httputil.NewSingleHostReverseProxy(targetUrl) + + // Modify the response headers and body + proxy.ModifyResponse = ResponseMesser + + // Start the reverse proxy server on the given address + fmt.Printf("Listening for proxy requests on %s\n", proxyAddr) + err = http.ListenAndServe(proxyAddr, proxy) + if err != nil { + panic(err) + } +} diff --git a/cmd/randomizer/messer.go b/cmd/randomizer/messer.go new file mode 100644 index 000000000..2b6d4d0ac --- /dev/null +++ b/cmd/randomizer/messer.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "math/rand" + "net/http" + "strconv" + "strings" +) + +func ResponseMesser(resp *http.Response) error { + // Swap two random bytes in the response headers + for k, v := range resp.Header { + // ignore most headers + kk := strings.ToLower(k) + if _, ok := messedHeaders[kk]; !ok { + fmt.Println("could not find:", kk, "in messedHeaders") + continue + } + + swapRandomStrings(v) + fmt.Println("messed:", k, v) + resp.Header[k] = v + } + + // randomly add headers that do not exists + for k, v := range addedHeaders { + if rand.Intn(10) > 1 || resp.Header.Get(k) != "" { + continue + } + + resp.Header[k] = []string{v} + } + + // Swap two random bytes in the response body + length := -1 + var err error + + // if resp has content length header, + // extract the new length and store it. + if resp.Header.Get("Content-Length") != "" { + cl := resp.Header.Get("Content-Length") + length, err = strconv.Atoi(cl) + if err != nil { + panic(err) + } + } + + swapRandomBytesReader := &swapRandomBytesReader{ + Reader: resp.Body, + } + + resp.Body = MyLimitReader(swapRandomBytesReader, int64(length)) + + resp.StatusCode = resp.StatusCode + 1 + + return nil +} + +func randPlaces(max int) (int, int) { + i := rand.Intn(max) + j := rand.Intn(max) + if i == j { + j = (j + 1) % max + } + + if i < j { + return i, j + } + return j, i +} + +// Shuffle bytes +func shuffleBytes(b []byte) { + for i := range b { + j := rand.Intn(i + 1) + b[i], b[j] = b[j], b[i] + } +} + +// Swaps two random bytes in the input +func swapRandomBytes(b []byte) { + if len(b) < 2 { + return + } + + i, j := randPlaces(len(b)) + b[i], b[j] = b[j], b[i] +} + +// Swaps two random characters in the input string +func swapRandomStrings(s []string) { + for i := range s { + s[i] = swapRandomString(s[i]) + } +} + +func swapRandomString(s string) string { + if len(s) < 2 { + return s + } + + i, j := randPlaces(len(s)) + + return s[:i] + string(s[j]) + s[i+1:j] + string(s[i]) + s[j+1:] +} + +func messRandomNumbers(s []string) { + for i := range s { + s[i] = messRandomNumber(s[i]) + } +} + +func messRandomNumber(s string) string { + d, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(err) + } + r := rand.Int63n(d) + + return strconv.FormatInt(r, 10) +} diff --git a/cmd/randomizer/streams.go b/cmd/randomizer/streams.go new file mode 100644 index 000000000..5a316c026 --- /dev/null +++ b/cmd/randomizer/streams.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "io" +) + +// A reader that swaps two random bytes in the input +type swapRandomBytesReader struct { + io.Reader + Length string +} + +func (r *swapRandomBytesReader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + + if err != nil && err != io.EOF { + return n, err + } + + fmt.Println("before:", string(p[:n])) + shuffleBytes(p[:n]) + fmt.Println("after:", string(p[:n])) + // swapRandomBytes(p[:n]) + return n, err +} + +func (r *swapRandomBytesReader) Close() error { + if closer, ok := r.Reader.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// LimitReader returns a Reader that reads from r +// but stops with EOF after n bytes. +// The underlying implementation is a *LimitedReader. +func MyLimitReader(r io.Reader, n int64) *MyLimitedReader { return &MyLimitedReader{r, n} } + +// A LimitedReader reads from R but limits the amount of +// data returned to just N bytes. Each call to Read +// updates N to reflect the new amount remaining. +// Read returns EOF when N <= 0 or when the underlying R returns EOF. +type MyLimitedReader struct { + R io.Reader // underlying reader + N int64 // max bytes remaining +} + +func (l *MyLimitedReader) Read(p []byte) (n int, err error) { + if l.N <= 0 { + return 0, io.EOF + } + if int64(len(p)) > l.N { + p = p[0:l.N] + } + n, err = l.R.Read(p) + l.N -= int64(n) + return +} + +func (r *MyLimitedReader) Close() error { + if closer, ok := r.R.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/fixtures/t0123/dag-pb.json b/fixtures/t0123/dag-pb.json deleted file mode 100644 index ab4f9f011..000000000 --- a/fixtures/t0123/dag-pb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Data": { - "/": { - "bytes": "CAE" - } - }, - "Links": [ - { - "Hash": { - "/": "bafybeidryarwh34ygbtyypbu7qjkl4euiwxby6cql6uvosonohkq2kwnkm" - }, - "Name": "foo", - "Tsize": 69 - }, - { - "Hash": { - "/": "bafkreic3ondyhizrzeoufvoodehinugpj3ecruwokaygl7elezhn2khqfa" - }, - "Name": "foo.txt", - "Tsize": 13 - } - ] -} diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..cbd96eefb --- /dev/null +++ b/test.sh @@ -0,0 +1,48 @@ +#! /usr/bin/env bash +set -x + +make test-kubo; mv output.json output-kubo.json +cat output-kubo.json | jq --raw-output 'select(.Action == "run") | .Package + " - " + .Test' | sort > test-kubo.run + +# if test-kubo.run is empty, something went wrong +if [ ! -s test-kubo.run ]; then + echo "test-kubo.run is empty" + exit 1 +fi + +make test-randomizer; mv output.json output-randomizer.json +cat output-randomizer.json | jq --raw-output 'select(.Action == "run") | .Package + " - " + .Test' | sort > test-randomizer.run +cat output-randomizer.json | jq --raw-output 'select(.Action == "pass") | .Package + " - " + .Test' | sort > test-randomizer.pass + +# detect tests that are not running during randomizer +# if there is a difference, something went wrong +diff test-kubo.run test-randomizer.run + +if [ $? -ne 0 ]; then + echo "test-kubo.run and test-randomizer.run are different" + exit 1 +fi + +# run randomizer test 5 times and detect the tests that are always passing: +cp test-randomizer.pass always_passing + +for i in {1..3}; do + # if always_passing is empty -> we're done. + if [ ! -s always_passing ]; then + break + fi + + make test-randomizer; mv output.json output-randomizer.json + cat output-randomizer.json | jq --raw-output 'select(.Action == "pass") | .Package + " - " + .Test' | sort > test-randomizer.pass + comm -12 always_passing test-randomizer.pass > temp.log + mv temp.log always_passing +done + +# if always_passing is not empty, something went wrong: +if [ -s always_passing ]; then + echo "always_passing is not empty" + exit 1 +else + echo "all tests failed at least once :clap:" + exit 0 +fi diff --git a/tests/t0114_gateway_subdomains_test.go b/tests/t0114_gateway_subdomains_test.go index ff1fb7194..dc12cffb0 100644 --- a/tests/t0114_gateway_subdomains_test.go +++ b/tests/t0114_gateway_subdomains_test.go @@ -371,6 +371,8 @@ func TestGatewaySubdomains(t *testing.T) { if SubdomainGateway.IsEnabled() { Run(t, tests) + } else { + t.Skip("Skipping subdomain gateway tests") } } diff --git a/tests/t0122_gateway_tar_test.go b/tests/t0122_gateway_tar_test.go index e5369b45a..5c10146ce 100644 --- a/tests/t0122_gateway_tar_test.go +++ b/tests/t0122_gateway_tar_test.go @@ -2,12 +2,8 @@ package tests import ( "testing" - - "github.com/ipfs/gateway-conformance/tooling/test" ) func TestGatewayTar(t *testing.T) { - tests := []test.CTest{} - - test.Run(t, tests) + t.Skip() } diff --git a/tooling/check/check.go b/tooling/check/check.go index 2741f0d57..dc25c6f90 100644 --- a/tooling/check/check.go +++ b/tooling/check/check.go @@ -295,7 +295,10 @@ func (c *CheckIsJSONEqual) Check(v []byte) CheckOutput { var o map[string]any err := json.Unmarshal(v, &o) if err != nil { - panic(err) // TODO: move a t.Testing around to call `t.Fatal` on this case + return CheckOutput{ + Success: false, + Reason: fmt.Sprintf("expected '%s' to be valid JSON, but got error: %s", string(v), err), + } } if reflect.DeepEqual(o, c.Value) { diff --git a/tooling/test/report.go b/tooling/test/report.go index 11b5df1d5..d67001c68 100644 --- a/tooling/test/report.go +++ b/tooling/test/report.go @@ -9,7 +9,6 @@ import ( "text/template" ) - type ReportInput struct { Req *http.Request Res *http.Response @@ -36,6 +35,23 @@ Actual Response: {{.Res | dump}} ` +// If the response is invalid (the content length > Body length for example), +// go's DumpResponse will panic. This function recover's from that panic and +// dumps the response again without the body. +func safeDumpResponse(res *http.Response) (b []byte, err error) { + if res == nil { + return []byte("nil"), nil + } + + b, err = httputil.DumpResponse(res, true) + + if err != nil { + b, err = httputil.DumpResponse(res, false) + } + + return b, err +} + func report(t *testing.T, test CTest, req *http.Request, res *http.Response, err error) { input := ReportInput{ Req: req, @@ -53,17 +69,14 @@ func report(t *testing.T, test CTest, req *http.Request, res *http.Response, err if v == nil { return "nil" } - + var b []byte var err error switch v := v.(type) { case *http.Request: b, err = httputil.DumpRequestOut(v, true) case *http.Response: - // TODO: we have to disable the body dump because - // it triggers an error: - // "http: ContentLength=6 with Body length 0" - b, err = httputil.DumpResponse(v, false) + b, err = safeDumpResponse(v) default: panic("unknown type") } diff --git a/tooling/test/test.go b/tooling/test/test.go index 1225ed1a1..78d2b694e 100644 --- a/tooling/test/test.go +++ b/tooling/test/test.go @@ -153,9 +153,11 @@ func Run(t *testing.T, tests []CTest) { } if test.Response.StatusCode != 0 { - if res.StatusCode != test.Response.StatusCode { - localReport(t, "Status code is not %d. It is %d", test.Response.StatusCode, res.StatusCode) - } + t.Run("Status code", func(t *testing.T) { + if res.StatusCode != test.Response.StatusCode { + localReport(t, "Status code is not %d. It is %d", test.Response.StatusCode, res.StatusCode) + } + }) } for key, value := range test.Response.Headers { @@ -187,37 +189,39 @@ func Run(t *testing.T, tests []CTest) { } if test.Response.Body != nil { - defer res.Body.Close() - resBody, err := io.ReadAll(res.Body) - if err != nil { - localReport(t, err) - } + t.Run("Body", func(t *testing.T) { + defer res.Body.Close() + resBody, err := io.ReadAll(res.Body) + if err != nil { + localReport(t, err) + } - var output check.CheckOutput + var output check.CheckOutput - switch v := test.Response.Body.(type) { - case check.Check[string]: - output = v.Check(string(resBody)) - case check.Check[[]byte]: - output = v.Check(resBody) - case string: - output = check.IsEqual(v).Check(string(resBody)) - case []byte: - output = check.IsEqualBytes(v).Check(resBody) - default: - output = check.CheckOutput{ - Success: false, - Reason: fmt.Sprintf("Body check has an invalid type: %T", test.Response.Body), + switch v := test.Response.Body.(type) { + case check.Check[string]: + output = v.Check(string(resBody)) + case check.Check[[]byte]: + output = v.Check(resBody) + case string: + output = check.IsEqual(v).Check(string(resBody)) + case []byte: + output = check.IsEqualBytes(v).Check(resBody) + default: + output = check.CheckOutput{ + Success: false, + Reason: fmt.Sprintf("Body check has an invalid type: %T", test.Response.Body), + } } - } - if !output.Success { - if output.Hint == "" { - localReport(t, "Body %s", output.Reason) - } else { - localReport(t, "Body %s (%s)", output.Reason, output.Hint) + if !output.Success { + if output.Hint == "" { + localReport(t, "Body %s", output.Reason) + } else { + localReport(t, "Body %s (%s)", output.Reason, output.Hint) + } } - } + }) } }) }