diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..847173a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, go: '1.26' } + - { os: windows-latest, go: '1.26' } + - { os: ubuntu-latest, go: '1.20' } + runs-on: ${{ matrix.os }} + env: + CGO_ENABLED: 0 + GOTOOLCHAIN: local + steps: + # Keep testdata byte-exact: autocrlf on Windows runners rewrites LF. + - name: Disable CRLF conversion + if: runner.os == 'Windows' + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go }} + # go.mod says go 1.20, so loopclosure flags upstream Go tests that assume + # per-iteration loop variables; the rest of vet stays on. + - name: Vet + if: matrix.go == '1.26' + run: go vet -loopclosure=false ./... + - name: Test + run: go test -count=1 ./... + + race: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: '1.26' + - name: Test with the race detector + run: go test -race -count=1 ./... diff --git a/httputil/reverseproxy_test.go b/httputil/reverseproxy_test.go index c694422..b6f4c0b 100644 --- a/httputil/reverseproxy_test.go +++ b/httputil/reverseproxy_test.go @@ -2094,6 +2094,14 @@ func testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuery bool, rawQuery: strings.Repeat("a=1&", 10000) + "a=1", cleanQuery: "", }} { + if wantCleanQuery && strings.Count(test.rawQuery, "&") >= 10000 && !urlLimitsQueryParams() { + // This fork's go.mod says go 1.20, so Go >= 1.24 runs it with + // GODEBUG=urlmaxqueryparams=0, and Go < 1.24 has no limit at + // all. Without the limit net/url parses every parameter, the + // proxy re-encodes all of them, and nothing is smuggled past + // the hook: the expectation only holds when the limit is on. + continue + } res, err := frontend.Client().Get(frontend.URL + "?" + test.rawQuery) if err != nil { t.Fatalf("Get: %v", err) @@ -2110,6 +2118,13 @@ func testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuery bool, } } +// urlLimitsQueryParams reports whether net/url rejects queries with more than +// 10000 parameters in this build (see testReverseProxyQueryParameterSmuggling). +func urlLimitsQueryParams() bool { + _, err := url.ParseQuery(strings.Repeat("a=1&", 10000) + "a=1") + return err != nil +} + // Issue #72954: We should not call WriteHeader on a ResponseWriter after hijacking // the connection. func TestReverseProxyHijackCopyError(t *testing.T) {