mirror of
https://github.com/refraction-networking/uquic.git
synced 2025-04-04 04:37:36 +03:00
init: separate from quic-go tree
This commit is contained in:
commit
10eaa8489c
482 changed files with 81633 additions and 1 deletions
42
.circleci/config.yml
Normal file
42
.circleci/config.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
version: 2.1
|
||||
executors:
|
||||
test-go120:
|
||||
docker:
|
||||
- image: "cimg/go:1.20"
|
||||
environment:
|
||||
runrace: true
|
||||
TIMESCALE_FACTOR: 3
|
||||
|
||||
jobs:
|
||||
"test": &test
|
||||
executor: test-go120
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Build infos"
|
||||
command: go version
|
||||
- run:
|
||||
name: "Run tools tests"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -race -r -v -randomize-all -trace integrationtests/tools
|
||||
- run:
|
||||
name: "Run self integration tests"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -v -randomize-all -trace integrationtests/self
|
||||
- run:
|
||||
name: "Run version negotiation tests"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -v -randomize-all -trace integrationtests/versionnegotiation
|
||||
- run:
|
||||
name: "Run self integration tests with race detector"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -race -v -randomize-all -trace integrationtests/self
|
||||
- run:
|
||||
name: "Run self integration tests with qlog"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -v -randomize-all -trace integrationtests/self -- -qlog
|
||||
- run:
|
||||
name: "Run version negotiation tests with qlog"
|
||||
command: go run github.com/onsi/ginkgo/v2/ginkgo -v -randomize-all -trace integrationtests/versionnegotiation -- -qlog
|
||||
go120:
|
||||
<<: *test
|
||||
|
||||
workflows:
|
||||
workflow:
|
||||
jobs:
|
||||
- go120
|
8
.githooks/README.md
Normal file
8
.githooks/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Git Hooks
|
||||
|
||||
This directory contains useful Git hooks for working with quic-go.
|
||||
|
||||
Install them by running
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
34
.githooks/pre-commit
Normal file
34
.githooks/pre-commit
Normal file
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Check that test files don't contain focussed test cases.
|
||||
errored=false
|
||||
for f in $(git diff --diff-filter=d --cached --name-only); do
|
||||
if [[ $f != *_test.go ]]; then continue; fi
|
||||
output=$(git show :"$f" | grep -n -e "FIt(" -e "FContext(" -e "FDescribe(")
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$f contains a focussed test:"
|
||||
echo "$output"
|
||||
echo ""
|
||||
errored=true
|
||||
fi
|
||||
done
|
||||
|
||||
pushd ./integrationtests/gomodvendor > /dev/null
|
||||
go mod tidy
|
||||
if [[ -n $(git diff --diff-filter=d --name-only -- "go.mod" "go.sum") ]]; then
|
||||
echo "go.mod / go.sum in integrationtests/gomodvendor not tidied"
|
||||
errored=true
|
||||
fi
|
||||
popd > /dev/null
|
||||
|
||||
# Check that all Go files are properly gofumpt-ed.
|
||||
output=$(gofumpt -d $(git diff --diff-filter=d --cached --name-only -- '*.go'))
|
||||
if [ -n "$output" ]; then
|
||||
echo "Found files that are not properly gofumpt-ed."
|
||||
echo "$output"
|
||||
errored=true
|
||||
fi
|
||||
|
||||
if [ "$errored" = true ]; then
|
||||
exit 1
|
||||
fi
|
27
.github/workflows/build-interop-docker.yml
vendored
Normal file
27
.github/workflows/build-interop-docker.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Build interop Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [ interop ]
|
||||
|
||||
jobs:
|
||||
interop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: "{{defaultContext}}:interop"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: martenseemann/quic-go-interop:latest
|
33
.github/workflows/cross-compile.sh
vendored
Normal file
33
.github/workflows/cross-compile.sh
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
dist="$1"
|
||||
goos=$(echo "$dist" | cut -d "/" -f1)
|
||||
goarch=$(echo "$dist" | cut -d "/" -f2)
|
||||
|
||||
# cross-compiling for android is a pain...
|
||||
if [[ "$goos" == "android" ]]; then exit; fi
|
||||
# iOS builds require Cgo, see https://github.com/golang/go/issues/43343
|
||||
# Cgo would then need a C cross compilation setup. Not worth the hassle.
|
||||
if [[ "$goos" == "ios" ]]; then exit; fi
|
||||
|
||||
# Write all log output to a temporary file instead of to stdout.
|
||||
# That allows running this script in parallel, while preserving the correct order of the output.
|
||||
log_file=$(mktemp)
|
||||
|
||||
error_handler() {
|
||||
cat "$log_file" >&2
|
||||
rm "$log_file"
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'error_handler' ERR
|
||||
|
||||
echo "$dist" >> "$log_file"
|
||||
out="main-$goos-$goarch"
|
||||
GOOS=$goos GOARCH=$goarch go build -o $out example/main.go >> "$log_file" 2>&1
|
||||
rm $out
|
||||
|
||||
cat "$log_file"
|
||||
rm "$log_file"
|
23
.github/workflows/cross-compile.yml
vendored
Normal file
23
.github/workflows/cross-compile.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
on: [push, pull_request]
|
||||
jobs:
|
||||
crosscompile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ "1.20.x", "1.21.0-rc.3" ]
|
||||
runs-on: ${{ fromJSON(vars['CROSS_COMPILE_RUNNER_UBUNTU'] || '"ubuntu-latest"') }}
|
||||
name: "Cross Compilation (Go ${{matrix.go}})"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install build utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-multilib
|
||||
- name: Install dependencies
|
||||
run: go build example/main.go
|
||||
- name: Run cross compilation
|
||||
# run in parallel on as many cores as are available on the machine
|
||||
run: go tool dist list | xargs -I % -P "$(nproc)" .github/workflows/cross-compile.sh %
|
23
.github/workflows/go-generate.sh
vendored
Normal file
23
.github/workflows/go-generate.sh
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
DIR=$(pwd)
|
||||
TMP=$(mktemp -d)
|
||||
cd "$TMP"
|
||||
cp -r "$DIR" orig
|
||||
cp -r "$DIR" generated
|
||||
|
||||
cd generated
|
||||
# delete all go-generated files generated (that adhere to the comment convention)
|
||||
grep --include \*.go -lrIZ "^// Code generated .* DO NOT EDIT\.$" . | xargs --null rm
|
||||
|
||||
# First regenerate sys_conn_buffers_write.go.
|
||||
# If it doesn't exist, the following mockgen calls will fail.
|
||||
go generate -run "sys_conn_buffers_write.go"
|
||||
# now generate everything
|
||||
go generate ./...
|
||||
cd ..
|
||||
|
||||
# don't compare fuzzing corpora
|
||||
diff --exclude=corpus --exclude=.git -ruN orig generated
|
13
.github/workflows/go-generate.yml
vendored
Normal file
13
.github/workflows/go-generate.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
on: [push, pull_request]
|
||||
jobs:
|
||||
gogenerate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.20.x"
|
||||
- name: Install dependencies
|
||||
run: go build
|
||||
- name: Run code generators
|
||||
run: .github/workflows/go-generate.sh
|
52
.github/workflows/integration.yml
vendored
Normal file
52
.github/workflows/integration.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ "1.20.x", "1.21.0-rc.3" ]
|
||||
runs-on: ${{ fromJSON(vars['INTEGRATION_RUNNER_UBUNTU'] || '"ubuntu-latest"') }}
|
||||
env:
|
||||
DEBUG: false # set this to true to export qlogs and save them as artifacts
|
||||
TIMESCALE_FACTOR: 3
|
||||
name: Integration Tests (Go ${{ matrix.go }})
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
stable: '!contains(${{ matrix.go }}, "beta") && !contains(${{ matrix.go }}, "rc")'
|
||||
go-version: ${{ matrix.go }}
|
||||
- run: go version
|
||||
- name: set qlogger
|
||||
if: env.DEBUG == 'true'
|
||||
run: echo "QLOGFLAG= -qlog" >> $GITHUB_ENV
|
||||
- name: Run other tests
|
||||
run: |
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace -skip-package self,versionnegotiation integrationtests
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/versionnegotiation -- ${{ env.QLOGFLAG }}
|
||||
- name: Run self tests, using QUIC v1
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/self -- -version=1 ${{ env.QLOGFLAG }}
|
||||
- name: Run self tests, using QUIC v2
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/self -- -version=2 ${{ env.QLOGFLAG }}
|
||||
- name: Run set tests, with GSO enabled
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
env:
|
||||
QUIC_GO_ENABLE_GSO: true
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/self -- -version=1 ${{ env.QLOGFLAG }}
|
||||
- name: Run tests (32 bit)
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
env:
|
||||
GOARCH: 386
|
||||
run: |
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace -skip-package self,versionnegotiation integrationtests
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/versionnegotiation -- ${{ env.QLOGFLAG }}
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo -r -v -randomize-all -randomize-suites -trace integrationtests/self -- ${{ env.QLOGFLAG }}
|
||||
- name: save qlogs
|
||||
if: ${{ always() && env.DEBUG == 'true' }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: qlogs
|
||||
path: integrationtests/self/*.qlog
|
73
.github/workflows/lint.yml
vendored
Normal file
73
.github/workflows/lint.yml
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
go-version: "1.20.x"
|
||||
- name: Check that no non-test files import Ginkgo or Gomega
|
||||
run: .github/workflows/no_ginkgo.sh
|
||||
- name: Check that go.mod is tidied
|
||||
run: |
|
||||
cp go.mod go.mod.orig
|
||||
cp go.sum go.sum.orig
|
||||
go mod tidy
|
||||
diff go.mod go.mod.orig
|
||||
diff go.sum go.sum.orig
|
||||
- name: Check that go mod vendor works
|
||||
run: |
|
||||
cd integrationtests/gomodvendor
|
||||
go mod vendor
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.20.x"
|
||||
- name: golangci-lint (Linux)
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
args: --timeout=3m
|
||||
version: v1.52.2
|
||||
- name: golangci-lint (Windows)
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
env:
|
||||
GOOS: "windows"
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
args: --timeout=3m
|
||||
version: v1.52.2
|
||||
- name: golangci-lint (OSX)
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
env:
|
||||
GOOS: "darwin"
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
args: --timeout=3m
|
||||
version: v1.52.2
|
||||
- name: golangci-lint (FreeBSD)
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
env:
|
||||
GOOS: "freebsd"
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
args: --timeout=3m
|
||||
version: v1.52.2
|
||||
- name: golangci-lint (others)
|
||||
if: success() || failure() # run this step even if the previous one failed
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
env:
|
||||
GOOS: "solaris" # some OS that we don't have any build tags for
|
||||
with:
|
||||
skip-pkg-cache: true
|
||||
args: --timeout=3m
|
||||
version: v1.52.2
|
24
.github/workflows/no_ginkgo.sh
vendored
Normal file
24
.github/workflows/no_ginkgo.sh
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Verify that no non-test files import Ginkgo or Gomega.
|
||||
|
||||
set -e
|
||||
|
||||
HAS_TESTING=false
|
||||
|
||||
cd ..
|
||||
for f in $(find . -name "*.go" ! -name "*_test.go" ! -name "tools.go"); do
|
||||
if grep -q "github.com/onsi/ginkgo" $f; then
|
||||
echo "$f imports github.com/onsi/ginkgo/v2"
|
||||
HAS_TESTING=true
|
||||
fi
|
||||
if grep -q "github.com/onsi/gomega" $f; then
|
||||
echo "$f imports github.com/onsi/gomega"
|
||||
HAS_TESTING=true
|
||||
fi
|
||||
done
|
||||
|
||||
if "$HAS_TESTING"; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
48
.github/workflows/unit.yml
vendored
Normal file
48
.github/workflows/unit.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ "ubuntu", "windows", "macos" ]
|
||||
go: [ "1.20.x", "1.21.0-rc.3" ]
|
||||
runs-on: ${{ fromJSON(vars[format('UNIT_RUNNER_{0}', matrix.os)] || format('"{0}-latest"', matrix.os)) }}
|
||||
name: Unit tests (${{ matrix.os}}, Go ${{ matrix.go }})
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- run: go version
|
||||
- name: Run tests
|
||||
env:
|
||||
TIMESCALE_FACTOR: 10
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -cover -randomize-all -randomize-suites -trace -skip-package integrationtests
|
||||
- name: Run tests as root
|
||||
if: ${{ matrix.os == 'ubuntu' }}
|
||||
env:
|
||||
TIMESCALE_FACTOR: 10
|
||||
FILE: sys_conn_helper_linux_test.go
|
||||
run: |
|
||||
test -f $FILE # make sure the file actually exists
|
||||
go run github.com/onsi/ginkgo/v2/ginkgo build -cover -tags root .
|
||||
sudo ./quic-go.test -ginkgo.v -ginkgo.trace -ginkgo.randomize-all -ginkgo.focus-file=$FILE -test.coverprofile coverage-root.txt
|
||||
rm quic-go.test
|
||||
- name: Run tests (32 bit)
|
||||
if: ${{ matrix.os != 'macos' }} # can't run 32 bit tests on OSX.
|
||||
env:
|
||||
TIMESCALE_FACTOR: 10
|
||||
GOARCH: 386
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -cover -coverprofile coverage.txt -output-dir . -randomize-all -randomize-suites -trace -skip-package integrationtests
|
||||
- name: Run tests with race detector
|
||||
if: ${{ matrix.os == 'ubuntu' }} # speed things up. Windows and OSX VMs are slow
|
||||
env:
|
||||
TIMESCALE_FACTOR: 20
|
||||
run: go run github.com/onsi/ginkgo/v2/ginkgo -r -v -race -randomize-all -randomize-suites -trace -skip-package integrationtests
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: coverage.txt,coverage-root.txt
|
||||
env_vars: OS=${{ matrix.os }}, GO=${{ matrix.go }}
|
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
debug
|
||||
debug.test
|
||||
main
|
||||
mockgen_tmp.go
|
||||
*.qtr
|
||||
*.qlog
|
||||
*.txt
|
||||
race.[0-9]*
|
||||
|
||||
fuzzing/*/*.zip
|
||||
fuzzing/*/coverprofile
|
||||
fuzzing/*/crashers
|
||||
fuzzing/*/sonarprofile
|
||||
fuzzing/*/suppressions
|
||||
fuzzing/*/corpus/
|
||||
|
||||
gomock_reflect_*/
|
44
.golangci.yml
Normal file
44
.golangci.yml
Normal file
|
@ -0,0 +1,44 @@
|
|||
run:
|
||||
skip-files:
|
||||
- internal/handshake/cipher_suite.go
|
||||
linters-settings:
|
||||
depguard:
|
||||
type: blacklist
|
||||
packages:
|
||||
- github.com/marten-seemann/qtls
|
||||
- github.com/quic-go/qtls-go1-19
|
||||
- github.com/quic-go/qtls-go1-20
|
||||
packages-with-error-message:
|
||||
- github.com/marten-seemann/qtls: "importing qtls only allowed in internal/qtls"
|
||||
- github.com/quic-go/qtls-go1-19: "importing qtls only allowed in internal/qtls"
|
||||
- github.com/quic-go/qtls-go1-20: "importing qtls only allowed in internal/qtls"
|
||||
misspell:
|
||||
ignore-words:
|
||||
- ect
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- depguard
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- goimports
|
||||
- gofmt # redundant, since gofmt *should* be a no-op after gofumpt
|
||||
- gofumpt
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- misspell
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- vet
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: internal/qtls
|
||||
linters:
|
||||
- depguard
|
109
Changelog.md
Normal file
109
Changelog.md
Normal file
|
@ -0,0 +1,109 @@
|
|||
# Changelog
|
||||
|
||||
## v0.22.0 (2021-07-25)
|
||||
|
||||
- Use `ReadBatch` to read multiple UDP packets from the socket with a single syscall
|
||||
- Add a config option (`Config.DisableVersionNegotiationPackets`) to disable sending of Version Negotiation packets
|
||||
- Drop support for QUIC draft versions 32 and 34
|
||||
- Remove the `RetireBugBackwardsCompatibilityMode`, which was intended to mitigate a bug when retiring connection IDs in quic-go in v0.17.2 and ealier
|
||||
|
||||
## v0.21.2 (2021-07-15)
|
||||
|
||||
- Update qtls (for Go 1.15, 1.16 and 1.17rc1) to include the fix for the crypto/tls panic (see https://groups.google.com/g/golang-dev/c/5LJ2V7rd-Ag/m/YGLHVBZ6AAAJ for details)
|
||||
|
||||
## v0.21.0 (2021-06-01)
|
||||
|
||||
- quic-go now supports RFC 9000!
|
||||
|
||||
## v0.20.0 (2021-03-19)
|
||||
|
||||
- Remove the `quic.Config.HandshakeTimeout`. Introduce a `quic.Config.HandshakeIdleTimeout`.
|
||||
|
||||
## v0.17.1 (2020-06-20)
|
||||
|
||||
- Supports QUIC WG draft-29.
|
||||
- Improve bundling of ACK frames (#2543).
|
||||
|
||||
## v0.16.0 (2020-05-31)
|
||||
|
||||
- Supports QUIC WG draft-28.
|
||||
|
||||
## v0.15.0 (2020-03-01)
|
||||
|
||||
- Supports QUIC WG draft-27.
|
||||
- Add support for 0-RTT.
|
||||
- Remove `Session.Close()`. Applications need to pass an application error code to the transport using `Session.CloseWithError()`.
|
||||
- Make the TLS Cipher Suites configurable (via `tls.Config.CipherSuites`).
|
||||
|
||||
## v0.14.0 (2019-12-04)
|
||||
|
||||
- Supports QUIC WG draft-24.
|
||||
|
||||
## v0.13.0 (2019-11-05)
|
||||
|
||||
- Supports QUIC WG draft-23.
|
||||
- Add an `EarlyListener` that allows sending of 0.5-RTT data.
|
||||
- Add a `TokenStore` to store address validation tokens.
|
||||
- Issue and use new connection IDs during a connection.
|
||||
|
||||
## v0.12.0 (2019-08-05)
|
||||
|
||||
- Implement HTTP/3.
|
||||
- Rename `quic.Cookie` to `quic.Token` and `quic.Config.AcceptCookie` to `quic.Config.AcceptToken`.
|
||||
- Distinguish between Retry tokens and tokens sent in NEW_TOKEN frames.
|
||||
- Enforce application protocol negotiation (via `tls.Config.NextProtos`).
|
||||
- Use a varint for error codes.
|
||||
- Add support for [quic-trace](https://github.com/google/quic-trace).
|
||||
- Add a context to `Listener.Accept`, `Session.Accept{Uni}Stream` and `Session.Open{Uni}StreamSync`.
|
||||
- Implement TLS key updates.
|
||||
|
||||
## v0.11.0 (2019-04-05)
|
||||
|
||||
- Drop support for gQUIC. For qQUIC support, please switch to the *gquic* branch.
|
||||
- Implement QUIC WG draft-19.
|
||||
- Use [qtls](https://github.com/marten-seemann/qtls) for TLS 1.3.
|
||||
- Return a `tls.ConnectionState` from `quic.Session.ConnectionState()`.
|
||||
- Remove the error return values from `quic.Stream.CancelRead()` and `quic.Stream.CancelWrite()`
|
||||
|
||||
## v0.10.0 (2018-08-28)
|
||||
|
||||
- Add support for QUIC 44, drop support for QUIC 42.
|
||||
|
||||
## v0.9.0 (2018-08-15)
|
||||
|
||||
- Add a `quic.Config` option for the length of the connection ID (for IETF QUIC).
|
||||
- Split Session.Close into one method for regular closing and one for closing with an error.
|
||||
|
||||
## v0.8.0 (2018-06-26)
|
||||
|
||||
- Add support for unidirectional streams (for IETF QUIC).
|
||||
- Add a `quic.Config` option for the maximum number of incoming streams.
|
||||
- Add support for QUIC 42 and 43.
|
||||
- Add dial functions that use a context.
|
||||
- Multiplex clients on a net.PacketConn, when using Dial(conn).
|
||||
|
||||
## v0.7.0 (2018-02-03)
|
||||
|
||||
- The lower boundary for packets included in ACKs is now derived, and the value sent in STOP_WAITING frames is ignored.
|
||||
- Remove `DialNonFWSecure` and `DialAddrNonFWSecure`.
|
||||
- Expose the `ConnectionState` in the `Session` (experimental API).
|
||||
- Implement packet pacing.
|
||||
|
||||
## v0.6.0 (2017-12-12)
|
||||
|
||||
- Add support for QUIC 39, drop support for QUIC 35 - 37
|
||||
- Added `quic.Config` options for maximal flow control windows
|
||||
- Add a `quic.Config` option for QUIC versions
|
||||
- Add a `quic.Config` option to request omission of the connection ID from a server
|
||||
- Add a `quic.Config` option to configure the source address validation
|
||||
- Add a `quic.Config` option to configure the handshake timeout
|
||||
- Add a `quic.Config` option to configure the idle timeout
|
||||
- Add a `quic.Config` option to configure keep-alive
|
||||
- Rename the STK to Cookie
|
||||
- Implement `net.Conn`-style deadlines for streams
|
||||
- Remove the `tls.Config` from the `quic.Config`. The `tls.Config` must now be passed to the `Dial` and `Listen` functions as a separate parameter. See the [Godoc](https://godoc.org/github.com/quic-go/quic-go) for details.
|
||||
- Changed the log level environment variable to only accept strings ("DEBUG", "INFO", "ERROR"), see [the wiki](https://github.com/quic-go/quic-go/wiki/Logging) for more details.
|
||||
- Rename the `h2quic.QuicRoundTripper` to `h2quic.RoundTripper`
|
||||
- Changed `h2quic.Server.Serve()` to accept a `net.PacketConn`
|
||||
- Drop support for Go 1.7 and 1.8.
|
||||
- Various bugfixes
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 the quic-go authors & Google, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -72,4 +72,4 @@ func getClientHelloSpec() *utls.ClientHelloSpec {
|
|||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Security Policy
|
||||
|
||||
quic-go still in development. This means that there may be problems in our protocols,
|
||||
or there may be mistakes in our implementations.
|
||||
We take security vulnerabilities very seriously. If you discover a security issue,
|
||||
please bring it to our attention right away!
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a vulnerability that may affect live deployments -- for example, by exposing
|
||||
a remote execution exploit -- please [**report privately**](https://github.com/quic-go/quic-go/security/advisories/new).
|
||||
Please **DO NOT file a public issue**.
|
||||
|
||||
If the issue is an implementation weakness that cannot be immediately exploited or
|
||||
something not yet deployed, just discuss it openly.
|
||||
|
||||
## Reporting a non security bug
|
||||
|
||||
For non-security bugs, please simply file a GitHub [issue](https://github.com/quic-go/quic-go/issues/new).
|
92
buffer_pool.go
Normal file
92
buffer_pool.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
)
|
||||
|
||||
type packetBuffer struct {
|
||||
Data []byte
|
||||
|
||||
// refCount counts how many packets Data is used in.
|
||||
// It doesn't support concurrent use.
|
||||
// It is > 1 when used for coalesced packet.
|
||||
refCount int
|
||||
}
|
||||
|
||||
// Split increases the refCount.
|
||||
// It must be called when a packet buffer is used for more than one packet,
|
||||
// e.g. when splitting coalesced packets.
|
||||
func (b *packetBuffer) Split() {
|
||||
b.refCount++
|
||||
}
|
||||
|
||||
// Decrement decrements the reference counter.
|
||||
// It doesn't put the buffer back into the pool.
|
||||
func (b *packetBuffer) Decrement() {
|
||||
b.refCount--
|
||||
if b.refCount < 0 {
|
||||
panic("negative packetBuffer refCount")
|
||||
}
|
||||
}
|
||||
|
||||
// MaybeRelease puts the packet buffer back into the pool,
|
||||
// if the reference counter already reached 0.
|
||||
func (b *packetBuffer) MaybeRelease() {
|
||||
// only put the packetBuffer back if it's not used any more
|
||||
if b.refCount == 0 {
|
||||
b.putBack()
|
||||
}
|
||||
}
|
||||
|
||||
// Release puts back the packet buffer into the pool.
|
||||
// It should be called when processing is definitely finished.
|
||||
func (b *packetBuffer) Release() {
|
||||
b.Decrement()
|
||||
if b.refCount != 0 {
|
||||
panic("packetBuffer refCount not zero")
|
||||
}
|
||||
b.putBack()
|
||||
}
|
||||
|
||||
// Len returns the length of Data
|
||||
func (b *packetBuffer) Len() protocol.ByteCount { return protocol.ByteCount(len(b.Data)) }
|
||||
func (b *packetBuffer) Cap() protocol.ByteCount { return protocol.ByteCount(cap(b.Data)) }
|
||||
|
||||
func (b *packetBuffer) putBack() {
|
||||
if cap(b.Data) == protocol.MaxPacketBufferSize {
|
||||
bufferPool.Put(b)
|
||||
return
|
||||
}
|
||||
if cap(b.Data) == protocol.MaxLargePacketBufferSize {
|
||||
largeBufferPool.Put(b)
|
||||
return
|
||||
}
|
||||
panic("putPacketBuffer called with packet of wrong size!")
|
||||
}
|
||||
|
||||
var bufferPool, largeBufferPool sync.Pool
|
||||
|
||||
func getPacketBuffer() *packetBuffer {
|
||||
buf := bufferPool.Get().(*packetBuffer)
|
||||
buf.refCount = 1
|
||||
buf.Data = buf.Data[:0]
|
||||
return buf
|
||||
}
|
||||
|
||||
func getLargePacketBuffer() *packetBuffer {
|
||||
buf := largeBufferPool.Get().(*packetBuffer)
|
||||
buf.refCount = 1
|
||||
buf.Data = buf.Data[:0]
|
||||
return buf
|
||||
}
|
||||
|
||||
func init() {
|
||||
bufferPool.New = func() any {
|
||||
return &packetBuffer{Data: make([]byte, 0, protocol.MaxPacketBufferSize)}
|
||||
}
|
||||
largeBufferPool.New = func() any {
|
||||
return &packetBuffer{Data: make([]byte, 0, protocol.MaxLargePacketBufferSize)}
|
||||
}
|
||||
}
|
59
buffer_pool_test.go
Normal file
59
buffer_pool_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Buffer Pool", func() {
|
||||
It("returns buffers of cap", func() {
|
||||
buf1 := getPacketBuffer()
|
||||
Expect(buf1.Data).To(HaveCap(protocol.MaxPacketBufferSize))
|
||||
buf2 := getLargePacketBuffer()
|
||||
Expect(buf2.Data).To(HaveCap(protocol.MaxLargePacketBufferSize))
|
||||
})
|
||||
|
||||
It("releases buffers", func() {
|
||||
buf1 := getPacketBuffer()
|
||||
buf1.Release()
|
||||
buf2 := getLargePacketBuffer()
|
||||
buf2.Release()
|
||||
})
|
||||
|
||||
It("gets the length", func() {
|
||||
buf := getPacketBuffer()
|
||||
buf.Data = append(buf.Data, []byte("foobar")...)
|
||||
Expect(buf.Len()).To(BeEquivalentTo(6))
|
||||
})
|
||||
|
||||
It("panics if wrong-sized buffers are passed", func() {
|
||||
buf := getPacketBuffer()
|
||||
buf.Data = make([]byte, 10)
|
||||
Expect(func() { buf.Release() }).To(Panic())
|
||||
})
|
||||
|
||||
It("panics if it is released twice", func() {
|
||||
buf := getPacketBuffer()
|
||||
buf.Release()
|
||||
Expect(func() { buf.Release() }).To(Panic())
|
||||
})
|
||||
|
||||
It("panics if it is decremented too many times", func() {
|
||||
buf := getPacketBuffer()
|
||||
buf.Decrement()
|
||||
Expect(func() { buf.Decrement() }).To(Panic())
|
||||
})
|
||||
|
||||
It("waits until all parts have been released", func() {
|
||||
buf := getPacketBuffer()
|
||||
buf.Split()
|
||||
buf.Split()
|
||||
// now we have 3 parts
|
||||
buf.Decrement()
|
||||
buf.Decrement()
|
||||
buf.Decrement()
|
||||
Expect(func() { buf.Decrement() }).To(Panic())
|
||||
})
|
||||
})
|
262
client.go
Normal file
262
client.go
Normal file
|
@ -0,0 +1,262 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
sendConn sendConn
|
||||
|
||||
use0RTT bool
|
||||
|
||||
packetHandlers packetHandlerManager
|
||||
onClose func()
|
||||
|
||||
tlsConf *tls.Config
|
||||
config *Config
|
||||
|
||||
connIDGenerator ConnectionIDGenerator
|
||||
srcConnID protocol.ConnectionID
|
||||
destConnID protocol.ConnectionID
|
||||
|
||||
initialPacketNumber protocol.PacketNumber
|
||||
hasNegotiatedVersion bool
|
||||
version protocol.VersionNumber
|
||||
|
||||
handshakeChan chan struct{}
|
||||
|
||||
conn quicConn
|
||||
|
||||
tracer logging.ConnectionTracer
|
||||
tracingID uint64
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
// make it possible to mock connection ID for initial generation in the tests
|
||||
var generateConnectionIDForInitial = protocol.GenerateConnectionIDForInitial
|
||||
var generateConnectionIDForInitialWithLength = protocol.GenerateConnectionIDForInitialWithLen
|
||||
|
||||
// DialAddr establishes a new QUIC connection to a server.
|
||||
// It resolves the address, and then creates a new UDP connection to dial the QUIC server.
|
||||
// When the QUIC connection is closed, this UDP connection is closed.
|
||||
// See Dial for more details.
|
||||
func DialAddr(ctx context.Context, addr string, tlsConf *tls.Config, conf *Config) (Connection, error) {
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dl, err := setupTransport(udpConn, tlsConf, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dl.Dial(ctx, udpAddr, tlsConf, conf)
|
||||
}
|
||||
|
||||
// DialAddrEarly establishes a new 0-RTT QUIC connection to a server.
|
||||
// See DialAddr for more details.
|
||||
func DialAddrEarly(ctx context.Context, addr string, tlsConf *tls.Config, conf *Config) (EarlyConnection, error) {
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dl, err := setupTransport(udpConn, tlsConf, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dl.DialEarly(ctx, udpAddr, tlsConf, conf)
|
||||
if err != nil {
|
||||
dl.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// DialEarly establishes a new 0-RTT QUIC connection to a server using a net.PacketConn.
|
||||
// See Dial for more details.
|
||||
func DialEarly(ctx context.Context, c net.PacketConn, addr net.Addr, tlsConf *tls.Config, conf *Config) (EarlyConnection, error) {
|
||||
dl, err := setupTransport(c, tlsConf, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dl.DialEarly(ctx, addr, tlsConf, conf)
|
||||
if err != nil {
|
||||
dl.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Dial establishes a new QUIC connection to a server using a net.PacketConn.
|
||||
// If the PacketConn satisfies the OOBCapablePacketConn interface (as a net.UDPConn does),
|
||||
// ECN and packet info support will be enabled. In this case, ReadMsgUDP and WriteMsgUDP
|
||||
// will be used instead of ReadFrom and WriteTo to read/write packets.
|
||||
// The tls.Config must define an application protocol (using NextProtos).
|
||||
//
|
||||
// This is a convenience function. More advanced use cases should instantiate a Transport,
|
||||
// which offers configuration options for a more fine-grained control of the connection establishment,
|
||||
// including reusing the underlying UDP socket for multiple QUIC connections.
|
||||
func Dial(ctx context.Context, c net.PacketConn, addr net.Addr, tlsConf *tls.Config, conf *Config) (Connection, error) {
|
||||
dl, err := setupTransport(c, tlsConf, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dl.Dial(ctx, addr, tlsConf, conf)
|
||||
if err != nil {
|
||||
dl.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func setupTransport(c net.PacketConn, tlsConf *tls.Config, createdPacketConn bool) (*Transport, error) {
|
||||
if tlsConf == nil {
|
||||
return nil, errors.New("quic: tls.Config not set")
|
||||
}
|
||||
return &Transport{
|
||||
Conn: c,
|
||||
createdConn: createdPacketConn,
|
||||
isSingleUse: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dial(
|
||||
ctx context.Context,
|
||||
conn sendConn,
|
||||
connIDGenerator ConnectionIDGenerator,
|
||||
packetHandlers packetHandlerManager,
|
||||
tlsConf *tls.Config,
|
||||
config *Config,
|
||||
onClose func(),
|
||||
use0RTT bool,
|
||||
) (quicConn, error) {
|
||||
c, err := newClient(conn, connIDGenerator, config, tlsConf, onClose, use0RTT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.packetHandlers = packetHandlers
|
||||
|
||||
c.tracingID = nextConnTracingID()
|
||||
if c.config.Tracer != nil {
|
||||
c.tracer = c.config.Tracer(context.WithValue(ctx, ConnectionTracingKey, c.tracingID), protocol.PerspectiveClient, c.destConnID)
|
||||
}
|
||||
if c.tracer != nil {
|
||||
c.tracer.StartedConnection(c.sendConn.LocalAddr(), c.sendConn.RemoteAddr(), c.srcConnID, c.destConnID)
|
||||
}
|
||||
|
||||
if err := c.dial(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.conn, nil
|
||||
}
|
||||
|
||||
func newClient(sendConn sendConn, connIDGenerator ConnectionIDGenerator, config *Config, tlsConf *tls.Config, onClose func(), use0RTT bool) (*client, error) {
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{}
|
||||
} else {
|
||||
tlsConf = tlsConf.Clone()
|
||||
}
|
||||
|
||||
srcConnID, err := connIDGenerator.GenerateConnectionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
destConnID, err := generateConnectionIDForInitial()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &client{
|
||||
connIDGenerator: connIDGenerator,
|
||||
srcConnID: srcConnID,
|
||||
destConnID: destConnID,
|
||||
sendConn: sendConn,
|
||||
use0RTT: use0RTT,
|
||||
onClose: onClose,
|
||||
tlsConf: tlsConf,
|
||||
config: config,
|
||||
version: config.Versions[0],
|
||||
handshakeChan: make(chan struct{}),
|
||||
logger: utils.DefaultLogger.WithPrefix("client"),
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *client) dial(ctx context.Context) error {
|
||||
c.logger.Infof("Starting new connection to %s (%s -> %s), source connection ID %s, destination connection ID %s, version %s", c.tlsConf.ServerName, c.sendConn.LocalAddr(), c.sendConn.RemoteAddr(), c.srcConnID, c.destConnID, c.version)
|
||||
|
||||
c.conn = newClientConnection(
|
||||
c.sendConn,
|
||||
c.packetHandlers,
|
||||
c.destConnID,
|
||||
c.srcConnID,
|
||||
c.connIDGenerator,
|
||||
c.config,
|
||||
c.tlsConf,
|
||||
c.initialPacketNumber,
|
||||
c.use0RTT,
|
||||
c.hasNegotiatedVersion,
|
||||
c.tracer,
|
||||
c.tracingID,
|
||||
c.logger,
|
||||
c.version,
|
||||
)
|
||||
|
||||
c.packetHandlers.Add(c.srcConnID, c.conn)
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
recreateChan := make(chan errCloseForRecreating)
|
||||
go func() {
|
||||
err := c.conn.run()
|
||||
var recreateErr *errCloseForRecreating
|
||||
if errors.As(err, &recreateErr) {
|
||||
recreateChan <- *recreateErr
|
||||
return
|
||||
}
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
errorChan <- err // returns as soon as the connection is closed
|
||||
}()
|
||||
|
||||
// only set when we're using 0-RTT
|
||||
// Otherwise, earlyConnChan will be nil. Receiving from a nil chan blocks forever.
|
||||
var earlyConnChan <-chan struct{}
|
||||
if c.use0RTT {
|
||||
earlyConnChan = c.conn.earlyConnReady()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.conn.shutdown()
|
||||
return ctx.Err()
|
||||
case err := <-errorChan:
|
||||
return err
|
||||
case recreateErr := <-recreateChan:
|
||||
c.initialPacketNumber = recreateErr.nextPacketNumber
|
||||
c.version = recreateErr.nextVersion
|
||||
c.hasNegotiatedVersion = true
|
||||
return c.dial(ctx)
|
||||
case <-earlyConnChan:
|
||||
// ready to send 0-RTT data
|
||||
return nil
|
||||
case <-c.conn.HandshakeComplete():
|
||||
// handshake successfully completed
|
||||
return nil
|
||||
}
|
||||
}
|
362
client_test.go
Normal file
362
client_test.go
Normal file
|
@ -0,0 +1,362 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type nullMultiplexer struct{}
|
||||
|
||||
func (n nullMultiplexer) AddConn(indexableConn) {}
|
||||
func (n nullMultiplexer) RemoveConn(indexableConn) error { return nil }
|
||||
|
||||
var _ = Describe("Client", func() {
|
||||
var (
|
||||
cl *client
|
||||
packetConn *MockSendConn
|
||||
connID protocol.ConnectionID
|
||||
origMultiplexer multiplexer
|
||||
tlsConf *tls.Config
|
||||
tracer *mocklogging.MockConnectionTracer
|
||||
config *Config
|
||||
|
||||
originalClientConnConstructor func(
|
||||
conn sendConn,
|
||||
runner connRunner,
|
||||
destConnID protocol.ConnectionID,
|
||||
srcConnID protocol.ConnectionID,
|
||||
connIDGenerator ConnectionIDGenerator,
|
||||
conf *Config,
|
||||
tlsConf *tls.Config,
|
||||
initialPacketNumber protocol.PacketNumber,
|
||||
enable0RTT bool,
|
||||
hasNegotiatedVersion bool,
|
||||
tracer logging.ConnectionTracer,
|
||||
tracingID uint64,
|
||||
logger utils.Logger,
|
||||
v protocol.VersionNumber,
|
||||
) quicConn
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
tlsConf = &tls.Config{NextProtos: []string{"proto1"}}
|
||||
connID = protocol.ParseConnectionID([]byte{0, 0, 0, 0, 0, 0, 0x13, 0x37})
|
||||
originalClientConnConstructor = newClientConnection
|
||||
tracer = mocklogging.NewMockConnectionTracer(mockCtrl)
|
||||
config = &Config{
|
||||
Tracer: func(ctx context.Context, perspective logging.Perspective, id ConnectionID) logging.ConnectionTracer {
|
||||
return tracer
|
||||
},
|
||||
Versions: []protocol.VersionNumber{protocol.Version1},
|
||||
}
|
||||
Eventually(areConnsRunning).Should(BeFalse())
|
||||
packetConn = NewMockSendConn(mockCtrl)
|
||||
packetConn.EXPECT().LocalAddr().Return(&net.UDPAddr{}).AnyTimes()
|
||||
packetConn.EXPECT().RemoteAddr().Return(&net.UDPAddr{}).AnyTimes()
|
||||
cl = &client{
|
||||
srcConnID: connID,
|
||||
destConnID: connID,
|
||||
version: protocol.Version1,
|
||||
sendConn: packetConn,
|
||||
tracer: tracer,
|
||||
logger: utils.DefaultLogger,
|
||||
}
|
||||
getMultiplexer() // make the sync.Once execute
|
||||
// replace the clientMuxer. getMultiplexer will now return the nullMultiplexer
|
||||
origMultiplexer = connMuxer
|
||||
connMuxer = &nullMultiplexer{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
connMuxer = origMultiplexer
|
||||
newClientConnection = originalClientConnConstructor
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if s, ok := cl.conn.(*connection); ok {
|
||||
s.shutdown()
|
||||
}
|
||||
Eventually(areConnsRunning).Should(BeFalse())
|
||||
})
|
||||
|
||||
Context("Dialing", func() {
|
||||
var origGenerateConnectionIDForInitial func() (protocol.ConnectionID, error)
|
||||
|
||||
BeforeEach(func() {
|
||||
origGenerateConnectionIDForInitial = generateConnectionIDForInitial
|
||||
generateConnectionIDForInitial = func() (protocol.ConnectionID, error) {
|
||||
return connID, nil
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
generateConnectionIDForInitial = origGenerateConnectionIDForInitial
|
||||
})
|
||||
|
||||
It("returns after the handshake is complete", func() {
|
||||
manager := NewMockPacketHandlerManager(mockCtrl)
|
||||
manager.EXPECT().Add(gomock.Any(), gomock.Any())
|
||||
|
||||
run := make(chan struct{})
|
||||
newClientConnection = func(
|
||||
_ sendConn,
|
||||
_ connRunner,
|
||||
_ protocol.ConnectionID,
|
||||
_ protocol.ConnectionID,
|
||||
_ ConnectionIDGenerator,
|
||||
_ *Config,
|
||||
_ *tls.Config,
|
||||
_ protocol.PacketNumber,
|
||||
enable0RTT bool,
|
||||
_ bool,
|
||||
_ logging.ConnectionTracer,
|
||||
_ uint64,
|
||||
_ utils.Logger,
|
||||
_ protocol.VersionNumber,
|
||||
) quicConn {
|
||||
Expect(enable0RTT).To(BeFalse())
|
||||
conn := NewMockQUICConn(mockCtrl)
|
||||
conn.EXPECT().run().Do(func() { close(run) })
|
||||
c := make(chan struct{})
|
||||
close(c)
|
||||
conn.EXPECT().HandshakeComplete().Return(c)
|
||||
return conn
|
||||
}
|
||||
cl, err := newClient(packetConn, &protocol.DefaultConnectionIDGenerator{}, populateConfig(config), tlsConf, nil, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cl.packetHandlers = manager
|
||||
Expect(cl).ToNot(BeNil())
|
||||
Expect(cl.dial(context.Background())).To(Succeed())
|
||||
Eventually(run).Should(BeClosed())
|
||||
})
|
||||
|
||||
It("returns early connections", func() {
|
||||
manager := NewMockPacketHandlerManager(mockCtrl)
|
||||
manager.EXPECT().Add(gomock.Any(), gomock.Any())
|
||||
readyChan := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
newClientConnection = func(
|
||||
_ sendConn,
|
||||
runner connRunner,
|
||||
_ protocol.ConnectionID,
|
||||
_ protocol.ConnectionID,
|
||||
_ ConnectionIDGenerator,
|
||||
_ *Config,
|
||||
_ *tls.Config,
|
||||
_ protocol.PacketNumber,
|
||||
enable0RTT bool,
|
||||
_ bool,
|
||||
_ logging.ConnectionTracer,
|
||||
_ uint64,
|
||||
_ utils.Logger,
|
||||
_ protocol.VersionNumber,
|
||||
) quicConn {
|
||||
Expect(enable0RTT).To(BeTrue())
|
||||
conn := NewMockQUICConn(mockCtrl)
|
||||
conn.EXPECT().run().Do(func() { close(done) })
|
||||
conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
|
||||
conn.EXPECT().earlyConnReady().Return(readyChan)
|
||||
return conn
|
||||
}
|
||||
|
||||
cl, err := newClient(packetConn, &protocol.DefaultConnectionIDGenerator{}, populateConfig(config), tlsConf, nil, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cl.packetHandlers = manager
|
||||
Expect(cl).ToNot(BeNil())
|
||||
Expect(cl.dial(context.Background())).To(Succeed())
|
||||
Eventually(done).Should(BeClosed())
|
||||
})
|
||||
|
||||
It("returns an error that occurs while waiting for the handshake to complete", func() {
|
||||
manager := NewMockPacketHandlerManager(mockCtrl)
|
||||
manager.EXPECT().Add(gomock.Any(), gomock.Any())
|
||||
|
||||
testErr := errors.New("early handshake error")
|
||||
newClientConnection = func(
|
||||
_ sendConn,
|
||||
_ connRunner,
|
||||
_ protocol.ConnectionID,
|
||||
_ protocol.ConnectionID,
|
||||
_ ConnectionIDGenerator,
|
||||
_ *Config,
|
||||
_ *tls.Config,
|
||||
_ protocol.PacketNumber,
|
||||
_ bool,
|
||||
_ bool,
|
||||
_ logging.ConnectionTracer,
|
||||
_ uint64,
|
||||
_ utils.Logger,
|
||||
_ protocol.VersionNumber,
|
||||
) quicConn {
|
||||
conn := NewMockQUICConn(mockCtrl)
|
||||
conn.EXPECT().run().Return(testErr)
|
||||
conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
|
||||
conn.EXPECT().earlyConnReady().Return(make(chan struct{}))
|
||||
return conn
|
||||
}
|
||||
var closed bool
|
||||
cl, err := newClient(packetConn, &protocol.DefaultConnectionIDGenerator{}, populateConfig(config), tlsConf, func() { closed = true }, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cl.packetHandlers = manager
|
||||
Expect(cl).ToNot(BeNil())
|
||||
Expect(cl.dial(context.Background())).To(MatchError(testErr))
|
||||
Expect(closed).To(BeTrue())
|
||||
})
|
||||
|
||||
Context("quic.Config", func() {
|
||||
It("setups with the right values", func() {
|
||||
tokenStore := NewLRUTokenStore(10, 4)
|
||||
config := &Config{
|
||||
HandshakeIdleTimeout: 1337 * time.Minute,
|
||||
MaxIdleTimeout: 42 * time.Hour,
|
||||
MaxIncomingStreams: 1234,
|
||||
MaxIncomingUniStreams: 4321,
|
||||
TokenStore: tokenStore,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
c := populateConfig(config)
|
||||
Expect(c.HandshakeIdleTimeout).To(Equal(1337 * time.Minute))
|
||||
Expect(c.MaxIdleTimeout).To(Equal(42 * time.Hour))
|
||||
Expect(c.MaxIncomingStreams).To(BeEquivalentTo(1234))
|
||||
Expect(c.MaxIncomingUniStreams).To(BeEquivalentTo(4321))
|
||||
Expect(c.TokenStore).To(Equal(tokenStore))
|
||||
Expect(c.EnableDatagrams).To(BeTrue())
|
||||
})
|
||||
|
||||
It("disables bidirectional streams", func() {
|
||||
config := &Config{
|
||||
MaxIncomingStreams: -1,
|
||||
MaxIncomingUniStreams: 4321,
|
||||
}
|
||||
c := populateConfig(config)
|
||||
Expect(c.MaxIncomingStreams).To(BeZero())
|
||||
Expect(c.MaxIncomingUniStreams).To(BeEquivalentTo(4321))
|
||||
})
|
||||
|
||||
It("disables unidirectional streams", func() {
|
||||
config := &Config{
|
||||
MaxIncomingStreams: 1234,
|
||||
MaxIncomingUniStreams: -1,
|
||||
}
|
||||
c := populateConfig(config)
|
||||
Expect(c.MaxIncomingStreams).To(BeEquivalentTo(1234))
|
||||
Expect(c.MaxIncomingUniStreams).To(BeZero())
|
||||
})
|
||||
|
||||
It("fills in default values if options are not set in the Config", func() {
|
||||
c := populateConfig(&Config{})
|
||||
Expect(c.Versions).To(Equal(protocol.SupportedVersions))
|
||||
Expect(c.HandshakeIdleTimeout).To(Equal(protocol.DefaultHandshakeIdleTimeout))
|
||||
Expect(c.MaxIdleTimeout).To(Equal(protocol.DefaultIdleTimeout))
|
||||
})
|
||||
})
|
||||
|
||||
It("creates new connections with the right parameters", func() {
|
||||
config := &Config{Versions: []protocol.VersionNumber{protocol.Version1}}
|
||||
c := make(chan struct{})
|
||||
var version protocol.VersionNumber
|
||||
var conf *Config
|
||||
done := make(chan struct{})
|
||||
newClientConnection = func(
|
||||
connP sendConn,
|
||||
_ connRunner,
|
||||
_ protocol.ConnectionID,
|
||||
_ protocol.ConnectionID,
|
||||
_ ConnectionIDGenerator,
|
||||
configP *Config,
|
||||
_ *tls.Config,
|
||||
_ protocol.PacketNumber,
|
||||
_ bool,
|
||||
_ bool,
|
||||
_ logging.ConnectionTracer,
|
||||
_ uint64,
|
||||
_ utils.Logger,
|
||||
versionP protocol.VersionNumber,
|
||||
) quicConn {
|
||||
version = versionP
|
||||
conf = configP
|
||||
close(c)
|
||||
// TODO: check connection IDs?
|
||||
conn := NewMockQUICConn(mockCtrl)
|
||||
conn.EXPECT().run()
|
||||
conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
|
||||
conn.EXPECT().destroy(gomock.Any()).MaxTimes(1)
|
||||
close(done)
|
||||
return conn
|
||||
}
|
||||
packetConn := NewMockPacketConn(mockCtrl)
|
||||
packetConn.EXPECT().ReadFrom(gomock.Any()).DoAndReturn(func([]byte) (int, net.Addr, error) {
|
||||
<-done
|
||||
return 0, nil, errors.New("closed")
|
||||
})
|
||||
packetConn.EXPECT().LocalAddr()
|
||||
packetConn.EXPECT().SetReadDeadline(gomock.Any()).AnyTimes()
|
||||
_, err := Dial(context.Background(), packetConn, &net.UDPAddr{}, tlsConf, config)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Eventually(c).Should(BeClosed())
|
||||
Expect(version).To(Equal(config.Versions[0]))
|
||||
Expect(conf.Versions).To(Equal(config.Versions))
|
||||
})
|
||||
|
||||
It("creates a new connections after version negotiation", func() {
|
||||
var counter int
|
||||
newClientConnection = func(
|
||||
_ sendConn,
|
||||
runner connRunner,
|
||||
_ protocol.ConnectionID,
|
||||
connID protocol.ConnectionID,
|
||||
_ ConnectionIDGenerator,
|
||||
configP *Config,
|
||||
_ *tls.Config,
|
||||
pn protocol.PacketNumber,
|
||||
_ bool,
|
||||
hasNegotiatedVersion bool,
|
||||
_ logging.ConnectionTracer,
|
||||
_ uint64,
|
||||
_ utils.Logger,
|
||||
versionP protocol.VersionNumber,
|
||||
) quicConn {
|
||||
conn := NewMockQUICConn(mockCtrl)
|
||||
conn.EXPECT().HandshakeComplete().Return(make(chan struct{}))
|
||||
if counter == 0 {
|
||||
Expect(pn).To(BeZero())
|
||||
Expect(hasNegotiatedVersion).To(BeFalse())
|
||||
conn.EXPECT().run().DoAndReturn(func() error {
|
||||
runner.Remove(connID)
|
||||
return &errCloseForRecreating{
|
||||
nextPacketNumber: 109,
|
||||
nextVersion: 789,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Expect(pn).To(Equal(protocol.PacketNumber(109)))
|
||||
Expect(hasNegotiatedVersion).To(BeTrue())
|
||||
conn.EXPECT().run()
|
||||
conn.EXPECT().destroy(gomock.Any())
|
||||
}
|
||||
counter++
|
||||
return conn
|
||||
}
|
||||
|
||||
config := &Config{Tracer: config.Tracer, Versions: []protocol.VersionNumber{protocol.Version1}}
|
||||
tracer.EXPECT().StartedConnection(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
|
||||
_, err := DialAddr(context.Background(), "localhost:7890", tlsConf, config)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(counter).To(Equal(2))
|
||||
})
|
||||
})
|
||||
})
|
64
closed_conn.go
Normal file
64
closed_conn.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"net"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
)
|
||||
|
||||
// A closedLocalConn is a connection that we closed locally.
|
||||
// When receiving packets for such a connection, we need to retransmit the packet containing the CONNECTION_CLOSE frame,
|
||||
// with an exponential backoff.
|
||||
type closedLocalConn struct {
|
||||
counter uint32
|
||||
perspective protocol.Perspective
|
||||
logger utils.Logger
|
||||
|
||||
sendPacket func(net.Addr, packetInfo)
|
||||
}
|
||||
|
||||
var _ packetHandler = &closedLocalConn{}
|
||||
|
||||
// newClosedLocalConn creates a new closedLocalConn and runs it.
|
||||
func newClosedLocalConn(sendPacket func(net.Addr, packetInfo), pers protocol.Perspective, logger utils.Logger) packetHandler {
|
||||
return &closedLocalConn{
|
||||
sendPacket: sendPacket,
|
||||
perspective: pers,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *closedLocalConn) handlePacket(p receivedPacket) {
|
||||
c.counter++
|
||||
// exponential backoff
|
||||
// only send a CONNECTION_CLOSE for the 1st, 2nd, 4th, 8th, 16th, ... packet arriving
|
||||
if bits.OnesCount32(c.counter) != 1 {
|
||||
return
|
||||
}
|
||||
c.logger.Debugf("Received %d packets after sending CONNECTION_CLOSE. Retransmitting.", c.counter)
|
||||
c.sendPacket(p.remoteAddr, p.info)
|
||||
}
|
||||
|
||||
func (c *closedLocalConn) shutdown() {}
|
||||
func (c *closedLocalConn) destroy(error) {}
|
||||
func (c *closedLocalConn) getPerspective() protocol.Perspective { return c.perspective }
|
||||
|
||||
// A closedRemoteConn is a connection that was closed remotely.
|
||||
// For such a connection, we might receive reordered packets that were sent before the CONNECTION_CLOSE.
|
||||
// We can just ignore those packets.
|
||||
type closedRemoteConn struct {
|
||||
perspective protocol.Perspective
|
||||
}
|
||||
|
||||
var _ packetHandler = &closedRemoteConn{}
|
||||
|
||||
func newClosedRemoteConn(pers protocol.Perspective) packetHandler {
|
||||
return &closedRemoteConn{perspective: pers}
|
||||
}
|
||||
|
||||
func (s *closedRemoteConn) handlePacket(receivedPacket) {}
|
||||
func (s *closedRemoteConn) shutdown() {}
|
||||
func (s *closedRemoteConn) destroy(error) {}
|
||||
func (s *closedRemoteConn) getPerspective() protocol.Perspective { return s.perspective }
|
38
closed_conn_test.go
Normal file
38
closed_conn_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Closed local connection", func() {
|
||||
It("tells its perspective", func() {
|
||||
conn := newClosedLocalConn(nil, protocol.PerspectiveClient, utils.DefaultLogger)
|
||||
Expect(conn.getPerspective()).To(Equal(protocol.PerspectiveClient))
|
||||
// stop the connection
|
||||
conn.shutdown()
|
||||
})
|
||||
|
||||
It("repeats the packet containing the CONNECTION_CLOSE frame", func() {
|
||||
written := make(chan net.Addr, 1)
|
||||
conn := newClosedLocalConn(
|
||||
func(addr net.Addr, _ packetInfo) { written <- addr },
|
||||
protocol.PerspectiveClient,
|
||||
utils.DefaultLogger,
|
||||
)
|
||||
addr := &net.UDPAddr{IP: net.IPv4(127, 1, 2, 3), Port: 1337}
|
||||
for i := 1; i <= 20; i++ {
|
||||
conn.handlePacket(receivedPacket{remoteAddr: addr})
|
||||
if i == 1 || i == 2 || i == 4 || i == 8 || i == 16 {
|
||||
Expect(written).To(Receive(Equal(addr))) // receive the CONNECTION_CLOSE
|
||||
} else {
|
||||
Expect(written).ToNot(Receive())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
23
codecov.yml
Normal file
23
codecov.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
coverage:
|
||||
round: nearest
|
||||
ignore:
|
||||
- streams_map_incoming_bidi.go
|
||||
- streams_map_incoming_uni.go
|
||||
- streams_map_outgoing_bidi.go
|
||||
- streams_map_outgoing_uni.go
|
||||
- http3/gzip_reader.go
|
||||
- interop/
|
||||
- internal/ackhandler/packet_linkedlist.go
|
||||
- internal/handshake/cipher_suite.go
|
||||
- internal/utils/byteinterval_linkedlist.go
|
||||
- internal/utils/newconnectionid_linkedlist.go
|
||||
- internal/utils/packetinterval_linkedlist.go
|
||||
- internal/utils/linkedlist/linkedlist.go
|
||||
- logging/null_tracer.go
|
||||
- fuzzing/
|
||||
- metrics/
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5
|
||||
patch: false
|
148
config.go
Normal file
148
config.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
// Clone clones a Config
|
||||
func (c *Config) Clone() *Config {
|
||||
copy := *c
|
||||
return ©
|
||||
}
|
||||
|
||||
func (c *Config) handshakeTimeout() time.Duration {
|
||||
return utils.Max(protocol.DefaultHandshakeTimeout, 2*c.HandshakeIdleTimeout)
|
||||
}
|
||||
|
||||
func validateConfig(config *Config) error {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
const maxStreams = 1 << 60
|
||||
if config.MaxIncomingStreams > maxStreams {
|
||||
config.MaxIncomingStreams = maxStreams
|
||||
}
|
||||
if config.MaxIncomingUniStreams > maxStreams {
|
||||
config.MaxIncomingUniStreams = maxStreams
|
||||
}
|
||||
if config.MaxStreamReceiveWindow > quicvarint.Max {
|
||||
config.MaxStreamReceiveWindow = quicvarint.Max
|
||||
}
|
||||
if config.MaxConnectionReceiveWindow > quicvarint.Max {
|
||||
config.MaxConnectionReceiveWindow = quicvarint.Max
|
||||
}
|
||||
// check that all QUIC versions are actually supported
|
||||
for _, v := range config.Versions {
|
||||
if !protocol.IsValidVersion(v) {
|
||||
return fmt.Errorf("invalid QUIC version: %s", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateServerConfig populates fields in the quic.Config with their default values, if none are set
|
||||
// it may be called with nil
|
||||
func populateServerConfig(config *Config) *Config {
|
||||
config = populateConfig(config)
|
||||
if config.MaxTokenAge == 0 {
|
||||
config.MaxTokenAge = protocol.TokenValidity
|
||||
}
|
||||
if config.MaxRetryTokenAge == 0 {
|
||||
config.MaxRetryTokenAge = protocol.RetryTokenValidity
|
||||
}
|
||||
if config.RequireAddressValidation == nil {
|
||||
config.RequireAddressValidation = func(net.Addr) bool { return false }
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// populateConfig populates fields in the quic.Config with their default values, if none are set
|
||||
// it may be called with nil
|
||||
func populateConfig(config *Config) *Config {
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
versions := config.Versions
|
||||
if len(versions) == 0 {
|
||||
versions = protocol.SupportedVersions
|
||||
}
|
||||
handshakeIdleTimeout := protocol.DefaultHandshakeIdleTimeout
|
||||
if config.HandshakeIdleTimeout != 0 {
|
||||
handshakeIdleTimeout = config.HandshakeIdleTimeout
|
||||
}
|
||||
idleTimeout := protocol.DefaultIdleTimeout
|
||||
if config.MaxIdleTimeout != 0 {
|
||||
idleTimeout = config.MaxIdleTimeout
|
||||
}
|
||||
initialStreamReceiveWindow := config.InitialStreamReceiveWindow
|
||||
if initialStreamReceiveWindow == 0 {
|
||||
initialStreamReceiveWindow = protocol.DefaultInitialMaxStreamData
|
||||
}
|
||||
maxStreamReceiveWindow := config.MaxStreamReceiveWindow
|
||||
if maxStreamReceiveWindow == 0 {
|
||||
maxStreamReceiveWindow = protocol.DefaultMaxReceiveStreamFlowControlWindow
|
||||
}
|
||||
initialConnectionReceiveWindow := config.InitialConnectionReceiveWindow
|
||||
if initialConnectionReceiveWindow == 0 {
|
||||
initialConnectionReceiveWindow = protocol.DefaultInitialMaxData
|
||||
}
|
||||
maxConnectionReceiveWindow := config.MaxConnectionReceiveWindow
|
||||
if maxConnectionReceiveWindow == 0 {
|
||||
maxConnectionReceiveWindow = protocol.DefaultMaxReceiveConnectionFlowControlWindow
|
||||
}
|
||||
maxIncomingStreams := config.MaxIncomingStreams
|
||||
if maxIncomingStreams == 0 {
|
||||
maxIncomingStreams = protocol.DefaultMaxIncomingStreams
|
||||
} else if maxIncomingStreams < 0 {
|
||||
maxIncomingStreams = 0
|
||||
}
|
||||
maxIncomingUniStreams := config.MaxIncomingUniStreams
|
||||
if maxIncomingUniStreams == 0 {
|
||||
maxIncomingUniStreams = protocol.DefaultMaxIncomingUniStreams
|
||||
} else if maxIncomingUniStreams < 0 {
|
||||
maxIncomingUniStreams = 0
|
||||
}
|
||||
|
||||
return &Config{
|
||||
GetConfigForClient: config.GetConfigForClient,
|
||||
Versions: versions,
|
||||
HandshakeIdleTimeout: handshakeIdleTimeout,
|
||||
MaxIdleTimeout: idleTimeout,
|
||||
MaxTokenAge: config.MaxTokenAge,
|
||||
MaxRetryTokenAge: config.MaxRetryTokenAge,
|
||||
RequireAddressValidation: config.RequireAddressValidation,
|
||||
KeepAlivePeriod: config.KeepAlivePeriod,
|
||||
InitialStreamReceiveWindow: initialStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: maxStreamReceiveWindow,
|
||||
InitialConnectionReceiveWindow: initialConnectionReceiveWindow,
|
||||
MaxConnectionReceiveWindow: maxConnectionReceiveWindow,
|
||||
AllowConnectionWindowIncrease: config.AllowConnectionWindowIncrease,
|
||||
MaxIncomingStreams: maxIncomingStreams,
|
||||
MaxIncomingUniStreams: maxIncomingUniStreams,
|
||||
TokenStore: config.TokenStore,
|
||||
EnableDatagrams: config.EnableDatagrams,
|
||||
DisablePathMTUDiscovery: config.DisablePathMTUDiscovery,
|
||||
DisableVersionNegotiationPackets: config.DisableVersionNegotiationPackets,
|
||||
Allow0RTT: config.Allow0RTT,
|
||||
Tracer: config.Tracer,
|
||||
}
|
||||
}
|
||||
|
||||
type PacketNumberLen = protocol.PacketNumberLen
|
||||
|
||||
const (
|
||||
// PacketNumberLen1 is a packet number length of 1 byte
|
||||
PacketNumberLen1 PacketNumberLen = 1
|
||||
// PacketNumberLen2 is a packet number length of 2 bytes
|
||||
PacketNumberLen2 PacketNumberLen = 2
|
||||
// PacketNumberLen3 is a packet number length of 3 bytes
|
||||
PacketNumberLen3 PacketNumberLen = 3
|
||||
// PacketNumberLen4 is a packet number length of 4 bytes
|
||||
PacketNumberLen4 PacketNumberLen = 4
|
||||
)
|
205
config_test.go
Normal file
205
config_test.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
Context("validating", func() {
|
||||
It("validates a nil config", func() {
|
||||
Expect(validateConfig(nil)).To(Succeed())
|
||||
})
|
||||
|
||||
It("validates a config with normal values", func() {
|
||||
conf := populateServerConfig(&Config{
|
||||
MaxIncomingStreams: 5,
|
||||
MaxStreamReceiveWindow: 10,
|
||||
})
|
||||
Expect(validateConfig(conf)).To(Succeed())
|
||||
Expect(conf.MaxIncomingStreams).To(BeEquivalentTo(5))
|
||||
Expect(conf.MaxStreamReceiveWindow).To(BeEquivalentTo(10))
|
||||
})
|
||||
|
||||
It("clips too large values for the stream limits", func() {
|
||||
conf := &Config{
|
||||
MaxIncomingStreams: 1<<60 + 1,
|
||||
MaxIncomingUniStreams: 1<<60 + 2,
|
||||
}
|
||||
Expect(validateConfig(conf)).To(Succeed())
|
||||
Expect(conf.MaxIncomingStreams).To(BeEquivalentTo(int64(1 << 60)))
|
||||
Expect(conf.MaxIncomingUniStreams).To(BeEquivalentTo(int64(1 << 60)))
|
||||
})
|
||||
|
||||
It("clips too large values for the flow control windows", func() {
|
||||
conf := &Config{
|
||||
MaxStreamReceiveWindow: quicvarint.Max + 1,
|
||||
MaxConnectionReceiveWindow: quicvarint.Max + 2,
|
||||
}
|
||||
Expect(validateConfig(conf)).To(Succeed())
|
||||
Expect(conf.MaxStreamReceiveWindow).To(BeEquivalentTo(uint64(quicvarint.Max)))
|
||||
Expect(conf.MaxConnectionReceiveWindow).To(BeEquivalentTo(uint64(quicvarint.Max)))
|
||||
})
|
||||
})
|
||||
|
||||
configWithNonZeroNonFunctionFields := func() *Config {
|
||||
c := &Config{}
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
|
||||
typ := v.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanSet() {
|
||||
// unexported field; not cloned.
|
||||
continue
|
||||
}
|
||||
|
||||
switch fn := typ.Field(i).Name; fn {
|
||||
case "GetConfigForClient", "RequireAddressValidation", "GetLogWriter", "AllowConnectionWindowIncrease", "Tracer":
|
||||
// Can't compare functions.
|
||||
case "Versions":
|
||||
f.Set(reflect.ValueOf([]VersionNumber{1, 2, 3}))
|
||||
case "ConnectionIDLength":
|
||||
f.Set(reflect.ValueOf(8))
|
||||
case "ConnectionIDGenerator":
|
||||
f.Set(reflect.ValueOf(&protocol.DefaultConnectionIDGenerator{ConnLen: protocol.DefaultConnectionIDLength}))
|
||||
case "HandshakeIdleTimeout":
|
||||
f.Set(reflect.ValueOf(time.Second))
|
||||
case "MaxIdleTimeout":
|
||||
f.Set(reflect.ValueOf(time.Hour))
|
||||
case "MaxTokenAge":
|
||||
f.Set(reflect.ValueOf(2 * time.Hour))
|
||||
case "MaxRetryTokenAge":
|
||||
f.Set(reflect.ValueOf(2 * time.Minute))
|
||||
case "TokenStore":
|
||||
f.Set(reflect.ValueOf(NewLRUTokenStore(2, 3)))
|
||||
case "InitialStreamReceiveWindow":
|
||||
f.Set(reflect.ValueOf(uint64(1234)))
|
||||
case "MaxStreamReceiveWindow":
|
||||
f.Set(reflect.ValueOf(uint64(9)))
|
||||
case "InitialConnectionReceiveWindow":
|
||||
f.Set(reflect.ValueOf(uint64(4321)))
|
||||
case "MaxConnectionReceiveWindow":
|
||||
f.Set(reflect.ValueOf(uint64(10)))
|
||||
case "MaxIncomingStreams":
|
||||
f.Set(reflect.ValueOf(int64(11)))
|
||||
case "MaxIncomingUniStreams":
|
||||
f.Set(reflect.ValueOf(int64(12)))
|
||||
case "StatelessResetKey":
|
||||
f.Set(reflect.ValueOf(&StatelessResetKey{1, 2, 3, 4}))
|
||||
case "KeepAlivePeriod":
|
||||
f.Set(reflect.ValueOf(time.Second))
|
||||
case "EnableDatagrams":
|
||||
f.Set(reflect.ValueOf(true))
|
||||
case "DisableVersionNegotiationPackets":
|
||||
f.Set(reflect.ValueOf(true))
|
||||
case "DisablePathMTUDiscovery":
|
||||
f.Set(reflect.ValueOf(true))
|
||||
case "Allow0RTT":
|
||||
f.Set(reflect.ValueOf(true))
|
||||
default:
|
||||
Fail(fmt.Sprintf("all fields must be accounted for, but saw unknown field %q", fn))
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
It("uses 10s handshake timeout for short handshake idle timeouts", func() {
|
||||
c := &Config{HandshakeIdleTimeout: time.Second}
|
||||
Expect(c.handshakeTimeout()).To(Equal(protocol.DefaultHandshakeTimeout))
|
||||
})
|
||||
|
||||
It("uses twice the handshake idle timeouts for the handshake timeout, for long handshake idle timeouts", func() {
|
||||
c := &Config{HandshakeIdleTimeout: time.Second * 11 / 2}
|
||||
Expect(c.handshakeTimeout()).To(Equal(11 * time.Second))
|
||||
})
|
||||
|
||||
Context("cloning", func() {
|
||||
It("clones function fields", func() {
|
||||
var calledAddrValidation, calledAllowConnectionWindowIncrease, calledTracer bool
|
||||
c1 := &Config{
|
||||
GetConfigForClient: func(info *ClientHelloInfo) (*Config, error) { return nil, errors.New("nope") },
|
||||
AllowConnectionWindowIncrease: func(Connection, uint64) bool { calledAllowConnectionWindowIncrease = true; return true },
|
||||
RequireAddressValidation: func(net.Addr) bool { calledAddrValidation = true; return true },
|
||||
Tracer: func(context.Context, logging.Perspective, ConnectionID) logging.ConnectionTracer {
|
||||
calledTracer = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c2 := c1.Clone()
|
||||
c2.RequireAddressValidation(&net.UDPAddr{})
|
||||
Expect(calledAddrValidation).To(BeTrue())
|
||||
c2.AllowConnectionWindowIncrease(nil, 1234)
|
||||
Expect(calledAllowConnectionWindowIncrease).To(BeTrue())
|
||||
_, err := c2.GetConfigForClient(&ClientHelloInfo{})
|
||||
Expect(err).To(MatchError("nope"))
|
||||
c2.Tracer(context.Background(), logging.PerspectiveClient, protocol.ConnectionID{})
|
||||
Expect(calledTracer).To(BeTrue())
|
||||
})
|
||||
|
||||
It("clones non-function fields", func() {
|
||||
c := configWithNonZeroNonFunctionFields()
|
||||
Expect(c.Clone()).To(Equal(c))
|
||||
})
|
||||
|
||||
It("returns a copy", func() {
|
||||
c1 := &Config{
|
||||
MaxIncomingStreams: 100,
|
||||
RequireAddressValidation: func(net.Addr) bool { return true },
|
||||
}
|
||||
c2 := c1.Clone()
|
||||
c2.MaxIncomingStreams = 200
|
||||
c2.RequireAddressValidation = func(net.Addr) bool { return false }
|
||||
|
||||
Expect(c1.MaxIncomingStreams).To(BeEquivalentTo(100))
|
||||
Expect(c1.RequireAddressValidation(&net.UDPAddr{})).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("populating", func() {
|
||||
It("populates function fields", func() {
|
||||
var calledAddrValidation bool
|
||||
c1 := &Config{}
|
||||
c1.RequireAddressValidation = func(net.Addr) bool { calledAddrValidation = true; return true }
|
||||
c2 := populateConfig(c1)
|
||||
c2.RequireAddressValidation(&net.UDPAddr{})
|
||||
Expect(calledAddrValidation).To(BeTrue())
|
||||
})
|
||||
|
||||
It("copies non-function fields", func() {
|
||||
c := configWithNonZeroNonFunctionFields()
|
||||
Expect(populateConfig(c)).To(Equal(c))
|
||||
})
|
||||
|
||||
It("populates empty fields with default values", func() {
|
||||
c := populateConfig(&Config{})
|
||||
Expect(c.Versions).To(Equal(protocol.SupportedVersions))
|
||||
Expect(c.HandshakeIdleTimeout).To(Equal(protocol.DefaultHandshakeIdleTimeout))
|
||||
Expect(c.InitialStreamReceiveWindow).To(BeEquivalentTo(protocol.DefaultInitialMaxStreamData))
|
||||
Expect(c.MaxStreamReceiveWindow).To(BeEquivalentTo(protocol.DefaultMaxReceiveStreamFlowControlWindow))
|
||||
Expect(c.InitialConnectionReceiveWindow).To(BeEquivalentTo(protocol.DefaultInitialMaxData))
|
||||
Expect(c.MaxConnectionReceiveWindow).To(BeEquivalentTo(protocol.DefaultMaxReceiveConnectionFlowControlWindow))
|
||||
Expect(c.MaxIncomingStreams).To(BeEquivalentTo(protocol.DefaultMaxIncomingStreams))
|
||||
Expect(c.MaxIncomingUniStreams).To(BeEquivalentTo(protocol.DefaultMaxIncomingUniStreams))
|
||||
Expect(c.DisableVersionNegotiationPackets).To(BeFalse())
|
||||
Expect(c.DisablePathMTUDiscovery).To(BeFalse())
|
||||
Expect(c.GetConfigForClient).To(BeNil())
|
||||
})
|
||||
|
||||
It("populates empty fields with default values, for the server", func() {
|
||||
c := populateServerConfig(&Config{})
|
||||
Expect(c.RequireAddressValidation).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
139
conn_id_generator.go
Normal file
139
conn_id_generator.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
type connIDGenerator struct {
|
||||
generator ConnectionIDGenerator
|
||||
highestSeq uint64
|
||||
|
||||
activeSrcConnIDs map[uint64]protocol.ConnectionID
|
||||
initialClientDestConnID *protocol.ConnectionID // nil for the client
|
||||
|
||||
addConnectionID func(protocol.ConnectionID)
|
||||
getStatelessResetToken func(protocol.ConnectionID) protocol.StatelessResetToken
|
||||
removeConnectionID func(protocol.ConnectionID)
|
||||
retireConnectionID func(protocol.ConnectionID)
|
||||
replaceWithClosed func([]protocol.ConnectionID, protocol.Perspective, []byte)
|
||||
queueControlFrame func(wire.Frame)
|
||||
}
|
||||
|
||||
func newConnIDGenerator(
|
||||
initialConnectionID protocol.ConnectionID,
|
||||
initialClientDestConnID *protocol.ConnectionID, // nil for the client
|
||||
addConnectionID func(protocol.ConnectionID),
|
||||
getStatelessResetToken func(protocol.ConnectionID) protocol.StatelessResetToken,
|
||||
removeConnectionID func(protocol.ConnectionID),
|
||||
retireConnectionID func(protocol.ConnectionID),
|
||||
replaceWithClosed func([]protocol.ConnectionID, protocol.Perspective, []byte),
|
||||
queueControlFrame func(wire.Frame),
|
||||
generator ConnectionIDGenerator,
|
||||
) *connIDGenerator {
|
||||
m := &connIDGenerator{
|
||||
generator: generator,
|
||||
activeSrcConnIDs: make(map[uint64]protocol.ConnectionID),
|
||||
addConnectionID: addConnectionID,
|
||||
getStatelessResetToken: getStatelessResetToken,
|
||||
removeConnectionID: removeConnectionID,
|
||||
retireConnectionID: retireConnectionID,
|
||||
replaceWithClosed: replaceWithClosed,
|
||||
queueControlFrame: queueControlFrame,
|
||||
}
|
||||
m.activeSrcConnIDs[0] = initialConnectionID
|
||||
m.initialClientDestConnID = initialClientDestConnID
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) SetMaxActiveConnIDs(limit uint64) error {
|
||||
if m.generator.ConnectionIDLen() == 0 {
|
||||
return nil
|
||||
}
|
||||
// The active_connection_id_limit transport parameter is the number of
|
||||
// connection IDs the peer will store. This limit includes the connection ID
|
||||
// used during the handshake, and the one sent in the preferred_address
|
||||
// transport parameter.
|
||||
// We currently don't send the preferred_address transport parameter,
|
||||
// so we can issue (limit - 1) connection IDs.
|
||||
for i := uint64(len(m.activeSrcConnIDs)); i < utils.Min(limit, protocol.MaxIssuedConnectionIDs); i++ {
|
||||
if err := m.issueNewConnID(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) Retire(seq uint64, sentWithDestConnID protocol.ConnectionID) error {
|
||||
if seq > m.highestSeq {
|
||||
return &qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: fmt.Sprintf("retired connection ID %d (highest issued: %d)", seq, m.highestSeq),
|
||||
}
|
||||
}
|
||||
connID, ok := m.activeSrcConnIDs[seq]
|
||||
// We might already have deleted this connection ID, if this is a duplicate frame.
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if connID == sentWithDestConnID {
|
||||
return &qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: fmt.Sprintf("retired connection ID %d (%s), which was used as the Destination Connection ID on this packet", seq, connID),
|
||||
}
|
||||
}
|
||||
m.retireConnectionID(connID)
|
||||
delete(m.activeSrcConnIDs, seq)
|
||||
// Don't issue a replacement for the initial connection ID.
|
||||
if seq == 0 {
|
||||
return nil
|
||||
}
|
||||
return m.issueNewConnID()
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) issueNewConnID() error {
|
||||
connID, err := m.generator.GenerateConnectionID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.activeSrcConnIDs[m.highestSeq+1] = connID
|
||||
m.addConnectionID(connID)
|
||||
m.queueControlFrame(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: m.highestSeq + 1,
|
||||
ConnectionID: connID,
|
||||
StatelessResetToken: m.getStatelessResetToken(connID),
|
||||
})
|
||||
m.highestSeq++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) SetHandshakeComplete() {
|
||||
if m.initialClientDestConnID != nil {
|
||||
m.retireConnectionID(*m.initialClientDestConnID)
|
||||
m.initialClientDestConnID = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) RemoveAll() {
|
||||
if m.initialClientDestConnID != nil {
|
||||
m.removeConnectionID(*m.initialClientDestConnID)
|
||||
}
|
||||
for _, connID := range m.activeSrcConnIDs {
|
||||
m.removeConnectionID(connID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *connIDGenerator) ReplaceWithClosed(pers protocol.Perspective, connClose []byte) {
|
||||
connIDs := make([]protocol.ConnectionID, 0, len(m.activeSrcConnIDs)+1)
|
||||
if m.initialClientDestConnID != nil {
|
||||
connIDs = append(connIDs, *m.initialClientDestConnID)
|
||||
}
|
||||
for _, connID := range m.activeSrcConnIDs {
|
||||
connIDs = append(connIDs, connID)
|
||||
}
|
||||
m.replaceWithClosed(connIDs, pers, connClose)
|
||||
}
|
189
conn_id_generator_test.go
Normal file
189
conn_id_generator_test.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Connection ID Generator", func() {
|
||||
var (
|
||||
addedConnIDs []protocol.ConnectionID
|
||||
retiredConnIDs []protocol.ConnectionID
|
||||
removedConnIDs []protocol.ConnectionID
|
||||
replacedWithClosed []protocol.ConnectionID
|
||||
queuedFrames []wire.Frame
|
||||
g *connIDGenerator
|
||||
)
|
||||
initialConnID := protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7})
|
||||
initialClientDestConnID := protocol.ParseConnectionID([]byte{0xa, 0xb, 0xc, 0xd, 0xe})
|
||||
|
||||
connIDToToken := func(c protocol.ConnectionID) protocol.StatelessResetToken {
|
||||
b := c.Bytes()[0]
|
||||
return protocol.StatelessResetToken{b, b, b, b, b, b, b, b, b, b, b, b, b, b, b, b}
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
addedConnIDs = nil
|
||||
retiredConnIDs = nil
|
||||
removedConnIDs = nil
|
||||
queuedFrames = nil
|
||||
replacedWithClosed = nil
|
||||
g = newConnIDGenerator(
|
||||
initialConnID,
|
||||
&initialClientDestConnID,
|
||||
func(c protocol.ConnectionID) { addedConnIDs = append(addedConnIDs, c) },
|
||||
connIDToToken,
|
||||
func(c protocol.ConnectionID) { removedConnIDs = append(removedConnIDs, c) },
|
||||
func(c protocol.ConnectionID) { retiredConnIDs = append(retiredConnIDs, c) },
|
||||
func(cs []protocol.ConnectionID, _ protocol.Perspective, _ []byte) {
|
||||
replacedWithClosed = append(replacedWithClosed, cs...)
|
||||
},
|
||||
func(f wire.Frame) { queuedFrames = append(queuedFrames, f) },
|
||||
&protocol.DefaultConnectionIDGenerator{ConnLen: initialConnID.Len()},
|
||||
)
|
||||
})
|
||||
|
||||
It("issues new connection IDs", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(4)).To(Succeed())
|
||||
Expect(retiredConnIDs).To(BeEmpty())
|
||||
Expect(addedConnIDs).To(HaveLen(3))
|
||||
for i := 0; i < len(addedConnIDs)-1; i++ {
|
||||
Expect(addedConnIDs[i]).ToNot(Equal(addedConnIDs[i+1]))
|
||||
}
|
||||
Expect(queuedFrames).To(HaveLen(3))
|
||||
for i := 0; i < 3; i++ {
|
||||
f := queuedFrames[i]
|
||||
Expect(f).To(BeAssignableToTypeOf(&wire.NewConnectionIDFrame{}))
|
||||
nf := f.(*wire.NewConnectionIDFrame)
|
||||
Expect(nf.SequenceNumber).To(BeEquivalentTo(i + 1))
|
||||
Expect(nf.ConnectionID.Len()).To(Equal(7))
|
||||
Expect(nf.StatelessResetToken).To(Equal(connIDToToken(nf.ConnectionID)))
|
||||
}
|
||||
})
|
||||
|
||||
It("limits the number of connection IDs that it issues", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(9999999)).To(Succeed())
|
||||
Expect(retiredConnIDs).To(BeEmpty())
|
||||
Expect(addedConnIDs).To(HaveLen(protocol.MaxIssuedConnectionIDs - 1))
|
||||
Expect(queuedFrames).To(HaveLen(protocol.MaxIssuedConnectionIDs - 1))
|
||||
})
|
||||
|
||||
// SetMaxActiveConnIDs is called twice when dialing a 0-RTT connection:
|
||||
// once for the restored from the old connections, once when we receive the transport parameters
|
||||
Context("dealing with 0-RTT", func() {
|
||||
It("doesn't issue new connection IDs when SetMaxActiveConnIDs is called with the same value", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(4)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(3))
|
||||
queuedFrames = nil
|
||||
Expect(g.SetMaxActiveConnIDs(4)).To(Succeed())
|
||||
Expect(queuedFrames).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("issues more connection IDs if the server allows a higher limit on the resumed connection", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(3)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(2))
|
||||
queuedFrames = nil
|
||||
Expect(g.SetMaxActiveConnIDs(6)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("issues more connection IDs if the server allows a higher limit on the resumed connection, when connection IDs were retired in between", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(3)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(2))
|
||||
queuedFrames = nil
|
||||
g.Retire(1, protocol.ConnectionID{})
|
||||
Expect(queuedFrames).To(HaveLen(1))
|
||||
queuedFrames = nil
|
||||
Expect(g.SetMaxActiveConnIDs(6)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(3))
|
||||
})
|
||||
})
|
||||
|
||||
It("errors if the peers tries to retire a connection ID that wasn't yet issued", func() {
|
||||
Expect(g.Retire(1, protocol.ConnectionID{})).To(MatchError(&qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: "retired connection ID 1 (highest issued: 0)",
|
||||
}))
|
||||
})
|
||||
|
||||
It("errors if the peers tries to retire a connection ID in a packet with that connection ID", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(4)).To(Succeed())
|
||||
Expect(queuedFrames).ToNot(BeEmpty())
|
||||
Expect(queuedFrames[0]).To(BeAssignableToTypeOf(&wire.NewConnectionIDFrame{}))
|
||||
f := queuedFrames[0].(*wire.NewConnectionIDFrame)
|
||||
Expect(g.Retire(f.SequenceNumber, f.ConnectionID)).To(MatchError(&qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: fmt.Sprintf("retired connection ID %d (%s), which was used as the Destination Connection ID on this packet", f.SequenceNumber, f.ConnectionID),
|
||||
}))
|
||||
})
|
||||
|
||||
It("issues new connection IDs, when old ones are retired", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(5)).To(Succeed())
|
||||
queuedFrames = nil
|
||||
Expect(retiredConnIDs).To(BeEmpty())
|
||||
Expect(g.Retire(3, protocol.ConnectionID{})).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(1))
|
||||
Expect(queuedFrames[0]).To(BeAssignableToTypeOf(&wire.NewConnectionIDFrame{}))
|
||||
nf := queuedFrames[0].(*wire.NewConnectionIDFrame)
|
||||
Expect(nf.SequenceNumber).To(BeEquivalentTo(5))
|
||||
Expect(nf.ConnectionID.Len()).To(Equal(7))
|
||||
})
|
||||
|
||||
It("retires the initial connection ID", func() {
|
||||
Expect(g.Retire(0, protocol.ConnectionID{})).To(Succeed())
|
||||
Expect(removedConnIDs).To(BeEmpty())
|
||||
Expect(retiredConnIDs).To(HaveLen(1))
|
||||
Expect(retiredConnIDs[0]).To(Equal(initialConnID))
|
||||
Expect(addedConnIDs).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("handles duplicate retirements", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(11)).To(Succeed())
|
||||
queuedFrames = nil
|
||||
Expect(retiredConnIDs).To(BeEmpty())
|
||||
Expect(g.Retire(5, protocol.ConnectionID{})).To(Succeed())
|
||||
Expect(retiredConnIDs).To(HaveLen(1))
|
||||
Expect(queuedFrames).To(HaveLen(1))
|
||||
Expect(g.Retire(5, protocol.ConnectionID{})).To(Succeed())
|
||||
Expect(retiredConnIDs).To(HaveLen(1))
|
||||
Expect(queuedFrames).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("retires the client's initial destination connection ID when the handshake completes", func() {
|
||||
g.SetHandshakeComplete()
|
||||
Expect(retiredConnIDs).To(HaveLen(1))
|
||||
Expect(retiredConnIDs[0]).To(Equal(initialClientDestConnID))
|
||||
})
|
||||
|
||||
It("removes all connection IDs", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(5)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(4))
|
||||
g.RemoveAll()
|
||||
Expect(removedConnIDs).To(HaveLen(6)) // initial conn ID, initial client dest conn id, and newly issued ones
|
||||
Expect(removedConnIDs).To(ContainElement(initialConnID))
|
||||
Expect(removedConnIDs).To(ContainElement(initialClientDestConnID))
|
||||
for _, f := range queuedFrames {
|
||||
nf := f.(*wire.NewConnectionIDFrame)
|
||||
Expect(removedConnIDs).To(ContainElement(nf.ConnectionID))
|
||||
}
|
||||
})
|
||||
|
||||
It("replaces with a closed connection for all connection IDs", func() {
|
||||
Expect(g.SetMaxActiveConnIDs(5)).To(Succeed())
|
||||
Expect(queuedFrames).To(HaveLen(4))
|
||||
g.ReplaceWithClosed(protocol.PerspectiveClient, []byte("foobar"))
|
||||
Expect(replacedWithClosed).To(HaveLen(6)) // initial conn ID, initial client dest conn id, and newly issued ones
|
||||
Expect(replacedWithClosed).To(ContainElement(initialClientDestConnID))
|
||||
Expect(replacedWithClosed).To(ContainElement(initialConnID))
|
||||
for _, f := range queuedFrames {
|
||||
nf := f.(*wire.NewConnectionIDFrame)
|
||||
Expect(replacedWithClosed).To(ContainElement(nf.ConnectionID))
|
||||
}
|
||||
})
|
||||
})
|
224
conn_id_manager.go
Normal file
224
conn_id_manager.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
list "github.com/quic-go/quic-go/internal/utils/linkedlist"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
type newConnID struct {
|
||||
SequenceNumber uint64
|
||||
ConnectionID protocol.ConnectionID
|
||||
StatelessResetToken protocol.StatelessResetToken
|
||||
}
|
||||
|
||||
type connIDManager struct {
|
||||
queue list.List[newConnID]
|
||||
|
||||
handshakeComplete bool
|
||||
activeSequenceNumber uint64
|
||||
highestRetired uint64
|
||||
activeConnectionID protocol.ConnectionID
|
||||
activeStatelessResetToken *protocol.StatelessResetToken
|
||||
|
||||
// We change the connection ID after sending on average
|
||||
// protocol.PacketsPerConnectionID packets. The actual value is randomized
|
||||
// hide the packet loss rate from on-path observers.
|
||||
rand utils.Rand
|
||||
packetsSinceLastChange uint32
|
||||
packetsPerConnectionID uint32
|
||||
|
||||
addStatelessResetToken func(protocol.StatelessResetToken)
|
||||
removeStatelessResetToken func(protocol.StatelessResetToken)
|
||||
queueControlFrame func(wire.Frame)
|
||||
|
||||
connectionIDLimit uint64 // [UQUIC] custom Connection ID limit
|
||||
}
|
||||
|
||||
func newConnIDManager(
|
||||
initialDestConnID protocol.ConnectionID,
|
||||
addStatelessResetToken func(protocol.StatelessResetToken),
|
||||
removeStatelessResetToken func(protocol.StatelessResetToken),
|
||||
queueControlFrame func(wire.Frame),
|
||||
) *connIDManager {
|
||||
return &connIDManager{
|
||||
activeConnectionID: initialDestConnID,
|
||||
addStatelessResetToken: addStatelessResetToken,
|
||||
removeStatelessResetToken: removeStatelessResetToken,
|
||||
queueControlFrame: queueControlFrame,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *connIDManager) AddFromPreferredAddress(connID protocol.ConnectionID, resetToken protocol.StatelessResetToken) error {
|
||||
return h.addConnectionID(1, connID, resetToken)
|
||||
}
|
||||
|
||||
func (h *connIDManager) Add(f *wire.NewConnectionIDFrame) error {
|
||||
if err := h.add(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// [UQUIC]
|
||||
connIDLimit := h.connectionIDLimit
|
||||
if connIDLimit == 0 {
|
||||
connIDLimit = protocol.MaxActiveConnectionIDs
|
||||
}
|
||||
// [/UQUIC]
|
||||
|
||||
if uint64(h.queue.Len()) >= connIDLimit {
|
||||
return &qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *connIDManager) add(f *wire.NewConnectionIDFrame) error {
|
||||
// If the NEW_CONNECTION_ID frame is reordered, such that its sequence number is smaller than the currently active
|
||||
// connection ID or if it was already retired, send the RETIRE_CONNECTION_ID frame immediately.
|
||||
if f.SequenceNumber < h.activeSequenceNumber || f.SequenceNumber < h.highestRetired {
|
||||
h.queueControlFrame(&wire.RetireConnectionIDFrame{
|
||||
SequenceNumber: f.SequenceNumber,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retire elements in the queue.
|
||||
// Doesn't retire the active connection ID.
|
||||
if f.RetirePriorTo > h.highestRetired {
|
||||
var next *list.Element[newConnID]
|
||||
for el := h.queue.Front(); el != nil; el = next {
|
||||
if el.Value.SequenceNumber >= f.RetirePriorTo {
|
||||
break
|
||||
}
|
||||
next = el.Next()
|
||||
h.queueControlFrame(&wire.RetireConnectionIDFrame{
|
||||
SequenceNumber: el.Value.SequenceNumber,
|
||||
})
|
||||
h.queue.Remove(el)
|
||||
}
|
||||
h.highestRetired = f.RetirePriorTo
|
||||
}
|
||||
|
||||
if f.SequenceNumber == h.activeSequenceNumber {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.addConnectionID(f.SequenceNumber, f.ConnectionID, f.StatelessResetToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retire the active connection ID, if necessary.
|
||||
if h.activeSequenceNumber < f.RetirePriorTo {
|
||||
// The queue is guaranteed to have at least one element at this point.
|
||||
h.updateConnectionID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *connIDManager) addConnectionID(seq uint64, connID protocol.ConnectionID, resetToken protocol.StatelessResetToken) error {
|
||||
// insert a new element at the end
|
||||
if h.queue.Len() == 0 || h.queue.Back().Value.SequenceNumber < seq {
|
||||
h.queue.PushBack(newConnID{
|
||||
SequenceNumber: seq,
|
||||
ConnectionID: connID,
|
||||
StatelessResetToken: resetToken,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
// insert a new element somewhere in the middle
|
||||
for el := h.queue.Front(); el != nil; el = el.Next() {
|
||||
if el.Value.SequenceNumber == seq {
|
||||
if el.Value.ConnectionID != connID {
|
||||
return fmt.Errorf("received conflicting connection IDs for sequence number %d", seq)
|
||||
}
|
||||
if el.Value.StatelessResetToken != resetToken {
|
||||
return fmt.Errorf("received conflicting stateless reset tokens for sequence number %d", seq)
|
||||
}
|
||||
break
|
||||
}
|
||||
if el.Value.SequenceNumber > seq {
|
||||
h.queue.InsertBefore(newConnID{
|
||||
SequenceNumber: seq,
|
||||
ConnectionID: connID,
|
||||
StatelessResetToken: resetToken,
|
||||
}, el)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *connIDManager) updateConnectionID() {
|
||||
h.queueControlFrame(&wire.RetireConnectionIDFrame{
|
||||
SequenceNumber: h.activeSequenceNumber,
|
||||
})
|
||||
h.highestRetired = utils.Max(h.highestRetired, h.activeSequenceNumber)
|
||||
if h.activeStatelessResetToken != nil {
|
||||
h.removeStatelessResetToken(*h.activeStatelessResetToken)
|
||||
}
|
||||
|
||||
front := h.queue.Remove(h.queue.Front())
|
||||
h.activeSequenceNumber = front.SequenceNumber
|
||||
h.activeConnectionID = front.ConnectionID
|
||||
h.activeStatelessResetToken = &front.StatelessResetToken
|
||||
h.packetsSinceLastChange = 0
|
||||
h.packetsPerConnectionID = protocol.PacketsPerConnectionID/2 + uint32(h.rand.Int31n(protocol.PacketsPerConnectionID))
|
||||
h.addStatelessResetToken(*h.activeStatelessResetToken)
|
||||
}
|
||||
|
||||
func (h *connIDManager) Close() {
|
||||
if h.activeStatelessResetToken != nil {
|
||||
h.removeStatelessResetToken(*h.activeStatelessResetToken)
|
||||
}
|
||||
}
|
||||
|
||||
// is called when the server performs a Retry
|
||||
// and when the server changes the connection ID in the first Initial sent
|
||||
func (h *connIDManager) ChangeInitialConnID(newConnID protocol.ConnectionID) {
|
||||
if h.activeSequenceNumber != 0 {
|
||||
panic("expected first connection ID to have sequence number 0")
|
||||
}
|
||||
h.activeConnectionID = newConnID
|
||||
}
|
||||
|
||||
// is called when the server provides a stateless reset token in the transport parameters
|
||||
func (h *connIDManager) SetStatelessResetToken(token protocol.StatelessResetToken) {
|
||||
if h.activeSequenceNumber != 0 {
|
||||
panic("expected first connection ID to have sequence number 0")
|
||||
}
|
||||
h.activeStatelessResetToken = &token
|
||||
h.addStatelessResetToken(token)
|
||||
}
|
||||
|
||||
func (h *connIDManager) SentPacket() {
|
||||
h.packetsSinceLastChange++
|
||||
}
|
||||
|
||||
func (h *connIDManager) shouldUpdateConnID() bool {
|
||||
if !h.handshakeComplete {
|
||||
return false
|
||||
}
|
||||
// initiate the first change as early as possible (after handshake completion)
|
||||
if h.queue.Len() > 0 && h.activeSequenceNumber == 0 {
|
||||
return true
|
||||
}
|
||||
// For later changes, only change if
|
||||
// 1. The queue of connection IDs is filled more than 50%.
|
||||
// 2. We sent at least PacketsPerConnectionID packets
|
||||
return 2*h.queue.Len() >= protocol.MaxActiveConnectionIDs &&
|
||||
h.packetsSinceLastChange >= h.packetsPerConnectionID
|
||||
}
|
||||
|
||||
func (h *connIDManager) Get() protocol.ConnectionID {
|
||||
if h.shouldUpdateConnID() {
|
||||
h.updateConnectionID()
|
||||
}
|
||||
return h.activeConnectionID
|
||||
}
|
||||
|
||||
func (h *connIDManager) SetHandshakeComplete() {
|
||||
h.handshakeComplete = true
|
||||
}
|
364
conn_id_manager_test.go
Normal file
364
conn_id_manager_test.go
Normal file
|
@ -0,0 +1,364 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Connection ID Manager", func() {
|
||||
var (
|
||||
m *connIDManager
|
||||
frameQueue []wire.Frame
|
||||
tokenAdded *protocol.StatelessResetToken
|
||||
removedTokens []protocol.StatelessResetToken
|
||||
)
|
||||
initialConnID := protocol.ParseConnectionID([]byte{0, 0, 0, 0})
|
||||
|
||||
BeforeEach(func() {
|
||||
frameQueue = nil
|
||||
tokenAdded = nil
|
||||
removedTokens = nil
|
||||
m = newConnIDManager(
|
||||
initialConnID,
|
||||
func(token protocol.StatelessResetToken) { tokenAdded = &token },
|
||||
func(token protocol.StatelessResetToken) { removedTokens = append(removedTokens, token) },
|
||||
func(f wire.Frame,
|
||||
) {
|
||||
frameQueue = append(frameQueue, f)
|
||||
})
|
||||
})
|
||||
|
||||
get := func() (protocol.ConnectionID, protocol.StatelessResetToken) {
|
||||
if m.queue.Len() == 0 {
|
||||
return protocol.ConnectionID{}, protocol.StatelessResetToken{}
|
||||
}
|
||||
val := m.queue.Remove(m.queue.Front())
|
||||
return val.ConnectionID, val.StatelessResetToken
|
||||
}
|
||||
|
||||
It("returns the initial connection ID", func() {
|
||||
Expect(m.Get()).To(Equal(initialConnID))
|
||||
})
|
||||
|
||||
It("changes the initial connection ID", func() {
|
||||
m.ChangeInitialConnID(protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5}))
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5})))
|
||||
})
|
||||
|
||||
It("sets the token for the first connection ID", func() {
|
||||
token := protocol.StatelessResetToken{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
m.SetStatelessResetToken(token)
|
||||
Expect(*m.activeStatelessResetToken).To(Equal(token))
|
||||
Expect(*tokenAdded).To(Equal(token))
|
||||
})
|
||||
|
||||
It("adds and gets connection IDs", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 10,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0xe, 0xd, 0xc, 0xb, 0xa, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
|
||||
})).To(Succeed())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 4,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
|
||||
})).To(Succeed())
|
||||
c1, rt1 := get()
|
||||
Expect(c1).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
Expect(rt1).To(Equal(protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe}))
|
||||
c2, rt2 := get()
|
||||
Expect(c2).To(Equal(protocol.ParseConnectionID([]byte{2, 3, 4, 5})))
|
||||
Expect(rt2).To(Equal(protocol.StatelessResetToken{0xe, 0xd, 0xc, 0xb, 0xa, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}))
|
||||
c3, _ := get()
|
||||
Expect(c3).To(BeZero())
|
||||
})
|
||||
|
||||
It("accepts duplicates", func() {
|
||||
f1 := &wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
|
||||
}
|
||||
f2 := &wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
|
||||
}
|
||||
Expect(m.Add(f1)).To(Succeed())
|
||||
Expect(m.Add(f2)).To(Succeed())
|
||||
c1, rt1 := get()
|
||||
Expect(c1).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
Expect(rt1).To(Equal(protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe}))
|
||||
c2, _ := get()
|
||||
Expect(c2).To(BeZero())
|
||||
})
|
||||
|
||||
It("ignores duplicates for the currently used connection ID", func() {
|
||||
f := &wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
|
||||
}
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Add(f)).To(Succeed())
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
c, _ := get()
|
||||
Expect(c).To(BeZero())
|
||||
// Now send the same connection ID again. It should not be queued.
|
||||
Expect(m.Add(f)).To(Succeed())
|
||||
c, _ = get()
|
||||
Expect(c).To(BeZero())
|
||||
})
|
||||
|
||||
It("rejects duplicates with different connection IDs", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 42,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
})).To(Succeed())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 42,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}),
|
||||
})).To(MatchError("received conflicting connection IDs for sequence number 42"))
|
||||
})
|
||||
|
||||
It("rejects duplicates with different connection IDs", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 42,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
|
||||
})).To(Succeed())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 42,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{0xe, 0xd, 0xc, 0xb, 0xa, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
|
||||
})).To(MatchError("received conflicting stateless reset tokens for sequence number 42"))
|
||||
})
|
||||
|
||||
It("retires connection IDs", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 10,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
})).To(Succeed())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 13,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}),
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(BeEmpty())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
RetirePriorTo: 14,
|
||||
SequenceNumber: 17,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{3, 4, 5, 6}),
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(HaveLen(3))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeEquivalentTo(10))
|
||||
Expect(frameQueue[1].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeEquivalentTo(13))
|
||||
Expect(frameQueue[2].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeZero())
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{3, 4, 5, 6})))
|
||||
})
|
||||
|
||||
It("ignores reordered connection IDs, if their sequence number was already retired", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 10,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
RetirePriorTo: 5,
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeZero())
|
||||
frameQueue = nil
|
||||
// If this NEW_CONNECTION_ID frame hadn't been reordered, we would have retired it before.
|
||||
// Make sure it gets retired immediately now.
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 4,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeEquivalentTo(4))
|
||||
})
|
||||
|
||||
It("ignores reordered connection IDs, if their sequence number was already retired or less than active", func() {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 10,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
|
||||
RetirePriorTo: 5,
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeZero())
|
||||
frameQueue = nil
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef})))
|
||||
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 9,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad}),
|
||||
RetirePriorTo: 5,
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeEquivalentTo(9))
|
||||
})
|
||||
|
||||
It("accepts retransmissions for the connection ID that is in use", func() {
|
||||
connID := protocol.ParseConnectionID([]byte{1, 2, 3, 4})
|
||||
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: connID,
|
||||
})).To(Succeed())
|
||||
m.SetHandshakeComplete()
|
||||
Expect(frameQueue).To(BeEmpty())
|
||||
Expect(m.Get()).To(Equal(connID))
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0]).To(BeAssignableToTypeOf(&wire.RetireConnectionIDFrame{}))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeZero())
|
||||
frameQueue = nil
|
||||
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: connID,
|
||||
})).To(Succeed())
|
||||
Expect(frameQueue).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("errors when the peer sends too connection IDs", func() {
|
||||
for i := uint8(1); i < protocol.MaxActiveConnectionIDs; i++ {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(i),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{i, i, i, i}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{i, i, i, i, i, i, i, i, i, i, i, i, i, i, i, i},
|
||||
})).To(Succeed())
|
||||
}
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(9999),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
|
||||
})).To(MatchError(&qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError}))
|
||||
})
|
||||
|
||||
It("initiates the first connection ID update as soon as possible", func() {
|
||||
Expect(m.Get()).To(Equal(initialConnID))
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
})).To(Succeed())
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
})
|
||||
|
||||
It("waits until handshake completion before initiating a connection ID update", func() {
|
||||
Expect(m.Get()).To(Equal(initialConnID))
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
})).To(Succeed())
|
||||
Expect(m.Get()).To(Equal(initialConnID))
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
})
|
||||
|
||||
It("initiates subsequent updates when enough packets are sent", func() {
|
||||
var s uint8
|
||||
for s = uint8(1); s < protocol.MaxActiveConnectionIDs; s++ {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(s),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{s, s, s, s}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s},
|
||||
})).To(Succeed())
|
||||
}
|
||||
|
||||
m.SetHandshakeComplete()
|
||||
lastConnID := m.Get()
|
||||
Expect(lastConnID).To(Equal(protocol.ParseConnectionID([]byte{1, 1, 1, 1})))
|
||||
|
||||
var counter int
|
||||
for i := 0; i < 50*protocol.PacketsPerConnectionID; i++ {
|
||||
m.SentPacket()
|
||||
|
||||
connID := m.Get()
|
||||
if connID != lastConnID {
|
||||
counter++
|
||||
lastConnID = connID
|
||||
Expect(removedTokens).To(HaveLen(1))
|
||||
removedTokens = nil
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(s),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{s, s, s, s}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s},
|
||||
})).To(Succeed())
|
||||
s++
|
||||
}
|
||||
}
|
||||
Expect(counter).To(BeNumerically("~", 50, 10))
|
||||
})
|
||||
|
||||
It("retires delayed connection IDs that arrive after a higher connection ID was already retired", func() {
|
||||
for s := uint8(10); s <= 10+protocol.MaxActiveConnectionIDs/2; s++ {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(s),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{s, s, s, s}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{s, s, s, s, s, s, s, s, s, s, s, s, s, s, s, s},
|
||||
})).To(Succeed())
|
||||
}
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{10, 10, 10, 10})))
|
||||
for {
|
||||
m.SentPacket()
|
||||
if m.Get() == protocol.ParseConnectionID([]byte{11, 11, 11, 11}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// The active conn ID is now {11, 11, 11, 11}
|
||||
Expect(m.queue.Front().Value.ConnectionID).To(Equal(protocol.ParseConnectionID([]byte{12, 12, 12, 12})))
|
||||
// Add a delayed connection ID. It should just be ignored now.
|
||||
frameQueue = nil
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(5),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{5, 5, 5, 5}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5},
|
||||
})).To(Succeed())
|
||||
Expect(m.queue.Front().Value.ConnectionID).To(Equal(protocol.ParseConnectionID([]byte{12, 12, 12, 12})))
|
||||
Expect(frameQueue).To(HaveLen(1))
|
||||
Expect(frameQueue[0].(*wire.RetireConnectionIDFrame).SequenceNumber).To(BeEquivalentTo(5))
|
||||
})
|
||||
|
||||
It("only initiates subsequent updates when enough if enough connection IDs are queued", func() {
|
||||
for i := uint8(1); i <= protocol.MaxActiveConnectionIDs/2; i++ {
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: uint64(i),
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{i, i, i, i}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{i, i, i, i, i, i, i, i, i, i, i, i, i, i, i, i},
|
||||
})).To(Succeed())
|
||||
}
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 1, 1, 1})))
|
||||
for i := 0; i < 2*protocol.PacketsPerConnectionID; i++ {
|
||||
m.SentPacket()
|
||||
}
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 1, 1, 1})))
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1337,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 3, 3, 7}),
|
||||
})).To(Succeed())
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{2, 2, 2, 2})))
|
||||
Expect(removedTokens).To(HaveLen(1))
|
||||
Expect(removedTokens[0]).To(Equal(protocol.StatelessResetToken{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}))
|
||||
})
|
||||
|
||||
It("removes the currently active stateless reset token when it is closed", func() {
|
||||
m.Close()
|
||||
Expect(removedTokens).To(BeEmpty())
|
||||
Expect(m.Add(&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 1,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
|
||||
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
})).To(Succeed())
|
||||
m.SetHandshakeComplete()
|
||||
Expect(m.Get()).To(Equal(protocol.ParseConnectionID([]byte{1, 2, 3, 4})))
|
||||
m.Close()
|
||||
Expect(removedTokens).To(HaveLen(1))
|
||||
Expect(removedTokens[0]).To(Equal(protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}))
|
||||
})
|
||||
})
|
2356
connection.go
Normal file
2356
connection.go
Normal file
File diff suppressed because it is too large
Load diff
3193
connection_test.go
Normal file
3193
connection_test.go
Normal file
File diff suppressed because it is too large
Load diff
51
connection_timer.go
Normal file
51
connection_timer.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
)
|
||||
|
||||
var deadlineSendImmediately = time.Time{}.Add(42 * time.Millisecond) // any value > time.Time{} and before time.Now() is fine
|
||||
|
||||
type connectionTimer struct {
|
||||
timer *utils.Timer
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func newTimer() *connectionTimer {
|
||||
return &connectionTimer{timer: utils.NewTimer()}
|
||||
}
|
||||
|
||||
func (t *connectionTimer) SetRead() {
|
||||
if deadline := t.timer.Deadline(); deadline != deadlineSendImmediately {
|
||||
t.last = deadline
|
||||
}
|
||||
t.timer.SetRead()
|
||||
}
|
||||
|
||||
func (t *connectionTimer) Chan() <-chan time.Time {
|
||||
return t.timer.Chan()
|
||||
}
|
||||
|
||||
// SetTimer resets the timer.
|
||||
// It makes sure that the deadline is strictly increasing.
|
||||
// This prevents busy-looping in cases where the timer fires, but we can't actually send out a packet.
|
||||
// This doesn't apply to the pacing deadline, which can be set multiple times to deadlineSendImmediately.
|
||||
func (t *connectionTimer) SetTimer(idleTimeoutOrKeepAlive, ackAlarm, lossTime, pacing time.Time) {
|
||||
deadline := idleTimeoutOrKeepAlive
|
||||
if !ackAlarm.IsZero() && ackAlarm.Before(deadline) && ackAlarm.After(t.last) {
|
||||
deadline = ackAlarm
|
||||
}
|
||||
if !lossTime.IsZero() && lossTime.Before(deadline) && lossTime.After(t.last) {
|
||||
deadline = lossTime
|
||||
}
|
||||
if !pacing.IsZero() && pacing.Before(deadline) {
|
||||
deadline = pacing
|
||||
}
|
||||
t.timer.Reset(deadline)
|
||||
}
|
||||
|
||||
func (t *connectionTimer) Stop() {
|
||||
t.timer.Stop()
|
||||
}
|
62
connection_timer_test.go
Normal file
62
connection_timer_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func (t *connectionTimer) Deadline() time.Time { return t.timer.Deadline() }
|
||||
|
||||
var _ = Describe("Timer", func() {
|
||||
It("sets an idle timeout", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), time.Time{}, time.Time{}, time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Hour)))
|
||||
})
|
||||
|
||||
It("sets an ACK timer", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), time.Time{}, time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Minute)))
|
||||
})
|
||||
|
||||
It("sets a loss timer", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), now.Add(time.Second), time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Second)))
|
||||
})
|
||||
|
||||
It("sets a pacing timer", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), now.Add(time.Second), now.Add(time.Millisecond))
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Millisecond)))
|
||||
})
|
||||
|
||||
It("doesn't reset to an earlier time", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), time.Time{}, time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Minute)))
|
||||
t.SetRead()
|
||||
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), time.Time{}, time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Hour)))
|
||||
})
|
||||
|
||||
It("allows the pacing timer to be set to send immediately", func() {
|
||||
now := time.Now()
|
||||
t := newTimer()
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), time.Time{}, time.Time{})
|
||||
Expect(t.Deadline()).To(Equal(now.Add(time.Minute)))
|
||||
t.SetRead()
|
||||
|
||||
t.SetTimer(now.Add(time.Hour), now.Add(time.Minute), time.Time{}, deadlineSendImmediately)
|
||||
Expect(t.Deadline()).To(Equal(deadlineSendImmediately))
|
||||
})
|
||||
})
|
107
crypto_stream.go
Normal file
107
crypto_stream.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
type cryptoStream interface {
|
||||
// for receiving data
|
||||
HandleCryptoFrame(*wire.CryptoFrame) error
|
||||
GetCryptoData() []byte
|
||||
Finish() error
|
||||
// for sending data
|
||||
io.Writer
|
||||
HasData() bool
|
||||
PopCryptoFrame(protocol.ByteCount) *wire.CryptoFrame
|
||||
}
|
||||
|
||||
type cryptoStreamImpl struct {
|
||||
queue *frameSorter
|
||||
msgBuf []byte
|
||||
|
||||
highestOffset protocol.ByteCount
|
||||
finished bool
|
||||
|
||||
writeOffset protocol.ByteCount
|
||||
writeBuf []byte
|
||||
}
|
||||
|
||||
func newCryptoStream() cryptoStream {
|
||||
return &cryptoStreamImpl{queue: newFrameSorter()}
|
||||
}
|
||||
|
||||
func (s *cryptoStreamImpl) HandleCryptoFrame(f *wire.CryptoFrame) error {
|
||||
highestOffset := f.Offset + protocol.ByteCount(len(f.Data))
|
||||
if maxOffset := highestOffset; maxOffset > protocol.MaxCryptoStreamOffset {
|
||||
return &qerr.TransportError{
|
||||
ErrorCode: qerr.CryptoBufferExceeded,
|
||||
ErrorMessage: fmt.Sprintf("received invalid offset %d on crypto stream, maximum allowed %d", maxOffset, protocol.MaxCryptoStreamOffset),
|
||||
}
|
||||
}
|
||||
if s.finished {
|
||||
if highestOffset > s.highestOffset {
|
||||
// reject crypto data received after this stream was already finished
|
||||
return &qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: "received crypto data after change of encryption level",
|
||||
}
|
||||
}
|
||||
// ignore data with a smaller offset than the highest received
|
||||
// could e.g. be a retransmission
|
||||
return nil
|
||||
}
|
||||
s.highestOffset = utils.Max(s.highestOffset, highestOffset)
|
||||
if err := s.queue.Push(f.Data, f.Offset, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
_, data, _ := s.queue.Pop()
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
s.msgBuf = append(s.msgBuf, data...)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCryptoData retrieves data that was received in CRYPTO frames
|
||||
func (s *cryptoStreamImpl) GetCryptoData() []byte {
|
||||
b := s.msgBuf
|
||||
s.msgBuf = nil
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *cryptoStreamImpl) Finish() error {
|
||||
if s.queue.HasMoreData() {
|
||||
return &qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: "encryption level changed, but crypto stream has more data to read",
|
||||
}
|
||||
}
|
||||
s.finished = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writes writes data that should be sent out in CRYPTO frames
|
||||
func (s *cryptoStreamImpl) Write(p []byte) (int, error) {
|
||||
s.writeBuf = append(s.writeBuf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (s *cryptoStreamImpl) HasData() bool {
|
||||
return len(s.writeBuf) > 0
|
||||
}
|
||||
|
||||
func (s *cryptoStreamImpl) PopCryptoFrame(maxLen protocol.ByteCount) *wire.CryptoFrame {
|
||||
f := &wire.CryptoFrame{Offset: s.writeOffset}
|
||||
n := utils.Min(f.MaxDataLen(maxLen), protocol.ByteCount(len(s.writeBuf)))
|
||||
f.Data = s.writeBuf[:n]
|
||||
s.writeBuf = s.writeBuf[n:]
|
||||
s.writeOffset += n
|
||||
return f
|
||||
}
|
82
crypto_stream_manager.go
Normal file
82
crypto_stream_manager.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/handshake"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
type cryptoDataHandler interface {
|
||||
HandleMessage([]byte, protocol.EncryptionLevel) error
|
||||
NextEvent() handshake.Event
|
||||
}
|
||||
|
||||
type cryptoStreamManager struct {
|
||||
cryptoHandler cryptoDataHandler
|
||||
|
||||
initialStream cryptoStream
|
||||
handshakeStream cryptoStream
|
||||
oneRTTStream cryptoStream
|
||||
}
|
||||
|
||||
func newCryptoStreamManager(
|
||||
cryptoHandler cryptoDataHandler,
|
||||
initialStream cryptoStream,
|
||||
handshakeStream cryptoStream,
|
||||
oneRTTStream cryptoStream,
|
||||
) *cryptoStreamManager {
|
||||
return &cryptoStreamManager{
|
||||
cryptoHandler: cryptoHandler,
|
||||
initialStream: initialStream,
|
||||
handshakeStream: handshakeStream,
|
||||
oneRTTStream: oneRTTStream,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cryptoStreamManager) HandleCryptoFrame(frame *wire.CryptoFrame, encLevel protocol.EncryptionLevel) error {
|
||||
var str cryptoStream
|
||||
//nolint:exhaustive // CRYPTO frames cannot be sent in 0-RTT packets.
|
||||
switch encLevel {
|
||||
case protocol.EncryptionInitial:
|
||||
str = m.initialStream
|
||||
case protocol.EncryptionHandshake:
|
||||
str = m.handshakeStream
|
||||
case protocol.Encryption1RTT:
|
||||
str = m.oneRTTStream
|
||||
default:
|
||||
return fmt.Errorf("received CRYPTO frame with unexpected encryption level: %s", encLevel)
|
||||
}
|
||||
if err := str.HandleCryptoFrame(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
data := str.GetCryptoData()
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
if err := m.cryptoHandler.HandleMessage(data, encLevel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cryptoStreamManager) GetPostHandshakeData(maxSize protocol.ByteCount) *wire.CryptoFrame {
|
||||
if !m.oneRTTStream.HasData() {
|
||||
return nil
|
||||
}
|
||||
return m.oneRTTStream.PopCryptoFrame(maxSize)
|
||||
}
|
||||
|
||||
func (m *cryptoStreamManager) Drop(encLevel protocol.EncryptionLevel) error {
|
||||
//nolint:exhaustive // 1-RTT keys should never get dropped.
|
||||
switch encLevel {
|
||||
case protocol.EncryptionInitial:
|
||||
return m.initialStream.Finish()
|
||||
case protocol.EncryptionHandshake:
|
||||
return m.handshakeStream.Finish()
|
||||
default:
|
||||
panic(fmt.Sprintf("dropped unexpected encryption level: %s", encLevel))
|
||||
}
|
||||
}
|
90
crypto_stream_manager_test.go
Normal file
90
crypto_stream_manager_test.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Crypto Stream Manager", func() {
|
||||
var (
|
||||
csm *cryptoStreamManager
|
||||
cs *MockCryptoDataHandler
|
||||
|
||||
initialStream *MockCryptoStream
|
||||
handshakeStream *MockCryptoStream
|
||||
oneRTTStream *MockCryptoStream
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
initialStream = NewMockCryptoStream(mockCtrl)
|
||||
handshakeStream = NewMockCryptoStream(mockCtrl)
|
||||
oneRTTStream = NewMockCryptoStream(mockCtrl)
|
||||
cs = NewMockCryptoDataHandler(mockCtrl)
|
||||
csm = newCryptoStreamManager(cs, initialStream, handshakeStream, oneRTTStream)
|
||||
})
|
||||
|
||||
It("passes messages to the initial stream", func() {
|
||||
cf := &wire.CryptoFrame{Data: []byte("foobar")}
|
||||
initialStream.EXPECT().HandleCryptoFrame(cf)
|
||||
initialStream.EXPECT().GetCryptoData().Return([]byte("foobar"))
|
||||
initialStream.EXPECT().GetCryptoData()
|
||||
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.EncryptionInitial)
|
||||
Expect(csm.HandleCryptoFrame(cf, protocol.EncryptionInitial)).To(Succeed())
|
||||
})
|
||||
|
||||
It("passes messages to the handshake stream", func() {
|
||||
cf := &wire.CryptoFrame{Data: []byte("foobar")}
|
||||
handshakeStream.EXPECT().HandleCryptoFrame(cf)
|
||||
handshakeStream.EXPECT().GetCryptoData().Return([]byte("foobar"))
|
||||
handshakeStream.EXPECT().GetCryptoData()
|
||||
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.EncryptionHandshake)
|
||||
Expect(csm.HandleCryptoFrame(cf, protocol.EncryptionHandshake)).To(Succeed())
|
||||
})
|
||||
|
||||
It("passes messages to the 1-RTT stream", func() {
|
||||
cf := &wire.CryptoFrame{Data: []byte("foobar")}
|
||||
oneRTTStream.EXPECT().HandleCryptoFrame(cf)
|
||||
oneRTTStream.EXPECT().GetCryptoData().Return([]byte("foobar"))
|
||||
oneRTTStream.EXPECT().GetCryptoData()
|
||||
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.Encryption1RTT)
|
||||
Expect(csm.HandleCryptoFrame(cf, protocol.Encryption1RTT)).To(Succeed())
|
||||
})
|
||||
|
||||
It("doesn't call the message handler, if there's no message", func() {
|
||||
cf := &wire.CryptoFrame{Data: []byte("foobar")}
|
||||
handshakeStream.EXPECT().HandleCryptoFrame(cf)
|
||||
handshakeStream.EXPECT().GetCryptoData() // don't return any data to handle
|
||||
// don't EXPECT any calls to HandleMessage()
|
||||
Expect(csm.HandleCryptoFrame(cf, protocol.EncryptionHandshake)).To(Succeed())
|
||||
})
|
||||
|
||||
It("processes all messages", func() {
|
||||
cf := &wire.CryptoFrame{Data: []byte("foobar")}
|
||||
handshakeStream.EXPECT().HandleCryptoFrame(cf)
|
||||
handshakeStream.EXPECT().GetCryptoData().Return([]byte("foo"))
|
||||
handshakeStream.EXPECT().GetCryptoData().Return([]byte("bar"))
|
||||
handshakeStream.EXPECT().GetCryptoData()
|
||||
cs.EXPECT().HandleMessage([]byte("foo"), protocol.EncryptionHandshake)
|
||||
cs.EXPECT().HandleMessage([]byte("bar"), protocol.EncryptionHandshake)
|
||||
Expect(csm.HandleCryptoFrame(cf, protocol.EncryptionHandshake)).To(Succeed())
|
||||
})
|
||||
|
||||
It("errors for unknown encryption levels", func() {
|
||||
err := csm.HandleCryptoFrame(&wire.CryptoFrame{}, 42)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("received CRYPTO frame with unexpected encryption level"))
|
||||
})
|
||||
|
||||
It("drops Initial", func() {
|
||||
initialStream.EXPECT().Finish()
|
||||
Expect(csm.Drop(protocol.EncryptionInitial)).To(Succeed())
|
||||
})
|
||||
|
||||
It("drops Handshake", func() {
|
||||
handshakeStream.EXPECT().Finish()
|
||||
Expect(csm.Drop(protocol.EncryptionHandshake)).To(Succeed())
|
||||
})
|
||||
})
|
140
crypto_stream_test.go
Normal file
140
crypto_stream_test.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Crypto Stream", func() {
|
||||
var str cryptoStream
|
||||
|
||||
BeforeEach(func() {
|
||||
str = newCryptoStream()
|
||||
})
|
||||
|
||||
Context("handling incoming data", func() {
|
||||
It("handles in-order CRYPTO frames", func() {
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")})).To(Succeed())
|
||||
Expect(str.GetCryptoData()).To(Equal([]byte("foo")))
|
||||
Expect(str.GetCryptoData()).To(BeNil())
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3})).To(Succeed())
|
||||
Expect(str.GetCryptoData()).To(Equal([]byte("bar")))
|
||||
Expect(str.GetCryptoData()).To(BeNil())
|
||||
})
|
||||
|
||||
It("errors if the frame exceeds the maximum offset", func() {
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
|
||||
Offset: protocol.MaxCryptoStreamOffset - 5,
|
||||
Data: []byte("foobar"),
|
||||
})).To(MatchError(&qerr.TransportError{
|
||||
ErrorCode: qerr.CryptoBufferExceeded,
|
||||
ErrorMessage: fmt.Sprintf("received invalid offset %d on crypto stream, maximum allowed %d", protocol.MaxCryptoStreamOffset+1, protocol.MaxCryptoStreamOffset),
|
||||
}))
|
||||
})
|
||||
|
||||
It("handles out-of-order CRYPTO frames", func() {
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Offset: 3, Data: []byte("bar")})).To(Succeed())
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")})).To(Succeed())
|
||||
Expect(str.GetCryptoData()).To(Equal([]byte("foobar")))
|
||||
Expect(str.GetCryptoData()).To(BeNil())
|
||||
})
|
||||
|
||||
Context("finishing", func() {
|
||||
It("errors if there's still data to read after finishing", func() {
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
|
||||
Data: []byte("foobar"),
|
||||
Offset: 10,
|
||||
})).To(Succeed())
|
||||
Expect(str.Finish()).To(MatchError(&qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: "encryption level changed, but crypto stream has more data to read",
|
||||
}))
|
||||
})
|
||||
|
||||
It("works with reordered data", func() {
|
||||
f1 := &wire.CryptoFrame{
|
||||
Data: []byte("foo"),
|
||||
}
|
||||
f2 := &wire.CryptoFrame{
|
||||
Offset: 3,
|
||||
Data: []byte("bar"),
|
||||
}
|
||||
Expect(str.HandleCryptoFrame(f2)).To(Succeed())
|
||||
Expect(str.HandleCryptoFrame(f1)).To(Succeed())
|
||||
Expect(str.Finish()).To(Succeed())
|
||||
Expect(str.HandleCryptoFrame(f2)).To(Succeed())
|
||||
})
|
||||
|
||||
It("rejects new crypto data after finishing", func() {
|
||||
Expect(str.Finish()).To(Succeed())
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
|
||||
Data: []byte("foo"),
|
||||
})).To(MatchError(&qerr.TransportError{
|
||||
ErrorCode: qerr.ProtocolViolation,
|
||||
ErrorMessage: "received crypto data after change of encryption level",
|
||||
}))
|
||||
})
|
||||
|
||||
It("ignores crypto data below the maximum offset received before finishing", func() {
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
|
||||
Data: []byte("foobar"),
|
||||
})).To(Succeed())
|
||||
Expect(str.GetCryptoData()).To(Equal([]byte("foobar")))
|
||||
Expect(str.Finish()).To(Succeed())
|
||||
Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
|
||||
Offset: 2,
|
||||
Data: []byte("foo"),
|
||||
})).To(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("writing data", func() {
|
||||
It("says if it has data", func() {
|
||||
Expect(str.HasData()).To(BeFalse())
|
||||
_, err := str.Write([]byte("foobar"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(str.HasData()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("pops crypto frames", func() {
|
||||
_, err := str.Write([]byte("foobar"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
f := str.PopCryptoFrame(1000)
|
||||
Expect(f).ToNot(BeNil())
|
||||
Expect(f.Offset).To(BeZero())
|
||||
Expect(f.Data).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("coalesces multiple writes", func() {
|
||||
_, err := str.Write([]byte("foo"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = str.Write([]byte("bar"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
f := str.PopCryptoFrame(1000)
|
||||
Expect(f).ToNot(BeNil())
|
||||
Expect(f.Offset).To(BeZero())
|
||||
Expect(f.Data).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("respects the maximum size", func() {
|
||||
frameHeaderLen := (&wire.CryptoFrame{}).Length(protocol.Version1)
|
||||
_, err := str.Write([]byte("foobar"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
f := str.PopCryptoFrame(frameHeaderLen + 3)
|
||||
Expect(f).ToNot(BeNil())
|
||||
Expect(f.Offset).To(BeZero())
|
||||
Expect(f.Data).To(Equal([]byte("foo")))
|
||||
f = str.PopCryptoFrame(frameHeaderLen + 3)
|
||||
Expect(f).ToNot(BeNil())
|
||||
Expect(f.Offset).To(Equal(protocol.ByteCount(3)))
|
||||
Expect(f.Data).To(Equal([]byte("bar")))
|
||||
})
|
||||
})
|
||||
})
|
126
datagram_queue.go
Normal file
126
datagram_queue.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
type datagramQueue struct {
|
||||
sendQueue chan *wire.DatagramFrame
|
||||
nextFrame *wire.DatagramFrame
|
||||
|
||||
rcvMx sync.Mutex
|
||||
rcvQueue [][]byte
|
||||
rcvd chan struct{} // used to notify Receive that a new datagram was received
|
||||
|
||||
closeErr error
|
||||
closed chan struct{}
|
||||
|
||||
hasData func()
|
||||
|
||||
dequeued chan struct{}
|
||||
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
func newDatagramQueue(hasData func(), logger utils.Logger) *datagramQueue {
|
||||
return &datagramQueue{
|
||||
hasData: hasData,
|
||||
sendQueue: make(chan *wire.DatagramFrame, 1),
|
||||
rcvd: make(chan struct{}, 1),
|
||||
dequeued: make(chan struct{}),
|
||||
closed: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AddAndWait queues a new DATAGRAM frame for sending.
|
||||
// It blocks until the frame has been dequeued.
|
||||
func (h *datagramQueue) AddAndWait(f *wire.DatagramFrame) error {
|
||||
select {
|
||||
case h.sendQueue <- f:
|
||||
h.hasData()
|
||||
case <-h.closed:
|
||||
return h.closeErr
|
||||
}
|
||||
|
||||
select {
|
||||
case <-h.dequeued:
|
||||
return nil
|
||||
case <-h.closed:
|
||||
return h.closeErr
|
||||
}
|
||||
}
|
||||
|
||||
// Peek gets the next DATAGRAM frame for sending.
|
||||
// If actually sent out, Pop needs to be called before the next call to Peek.
|
||||
func (h *datagramQueue) Peek() *wire.DatagramFrame {
|
||||
if h.nextFrame != nil {
|
||||
return h.nextFrame
|
||||
}
|
||||
select {
|
||||
case h.nextFrame = <-h.sendQueue:
|
||||
h.dequeued <- struct{}{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return h.nextFrame
|
||||
}
|
||||
|
||||
func (h *datagramQueue) Pop() {
|
||||
if h.nextFrame == nil {
|
||||
panic("datagramQueue BUG: Pop called for nil frame")
|
||||
}
|
||||
h.nextFrame = nil
|
||||
}
|
||||
|
||||
// HandleDatagramFrame handles a received DATAGRAM frame.
|
||||
func (h *datagramQueue) HandleDatagramFrame(f *wire.DatagramFrame) {
|
||||
data := make([]byte, len(f.Data))
|
||||
copy(data, f.Data)
|
||||
var queued bool
|
||||
h.rcvMx.Lock()
|
||||
if len(h.rcvQueue) < protocol.DatagramRcvQueueLen {
|
||||
h.rcvQueue = append(h.rcvQueue, data)
|
||||
queued = true
|
||||
select {
|
||||
case h.rcvd <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
h.rcvMx.Unlock()
|
||||
if !queued && h.logger.Debug() {
|
||||
h.logger.Debugf("Discarding DATAGRAM frame (%d bytes payload)", len(f.Data))
|
||||
}
|
||||
}
|
||||
|
||||
// Receive gets a received DATAGRAM frame.
|
||||
func (h *datagramQueue) Receive(ctx context.Context) ([]byte, error) {
|
||||
for {
|
||||
h.rcvMx.Lock()
|
||||
if len(h.rcvQueue) > 0 {
|
||||
data := h.rcvQueue[0]
|
||||
h.rcvQueue = h.rcvQueue[1:]
|
||||
h.rcvMx.Unlock()
|
||||
return data, nil
|
||||
}
|
||||
h.rcvMx.Unlock()
|
||||
select {
|
||||
case <-h.rcvd:
|
||||
continue
|
||||
case <-h.closed:
|
||||
return nil, h.closeErr
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *datagramQueue) CloseWithError(e error) {
|
||||
h.closeErr = e
|
||||
close(h.closed)
|
||||
}
|
134
datagram_queue_test.go
Normal file
134
datagram_queue_test.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Datagram Queue", func() {
|
||||
var queue *datagramQueue
|
||||
var queued chan struct{}
|
||||
|
||||
BeforeEach(func() {
|
||||
queued = make(chan struct{}, 100)
|
||||
queue = newDatagramQueue(func() { queued <- struct{}{} }, utils.DefaultLogger)
|
||||
})
|
||||
|
||||
Context("sending", func() {
|
||||
It("returns nil when there's no datagram to send", func() {
|
||||
Expect(queue.Peek()).To(BeNil())
|
||||
})
|
||||
|
||||
It("queues a datagram", func() {
|
||||
done := make(chan struct{})
|
||||
frame := &wire.DatagramFrame{Data: []byte("foobar")}
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer close(done)
|
||||
Expect(queue.AddAndWait(frame)).To(Succeed())
|
||||
}()
|
||||
|
||||
Eventually(queued).Should(HaveLen(1))
|
||||
Consistently(done).ShouldNot(BeClosed())
|
||||
f := queue.Peek()
|
||||
Expect(f.Data).To(Equal([]byte("foobar")))
|
||||
Eventually(done).Should(BeClosed())
|
||||
queue.Pop()
|
||||
Expect(queue.Peek()).To(BeNil())
|
||||
})
|
||||
|
||||
It("returns the same datagram multiple times, when Pop isn't called", func() {
|
||||
sent := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
Expect(queue.AddAndWait(&wire.DatagramFrame{Data: []byte("foo")})).To(Succeed())
|
||||
sent <- struct{}{}
|
||||
Expect(queue.AddAndWait(&wire.DatagramFrame{Data: []byte("bar")})).To(Succeed())
|
||||
sent <- struct{}{}
|
||||
}()
|
||||
|
||||
Eventually(queued).Should(HaveLen(1))
|
||||
f := queue.Peek()
|
||||
Expect(f.Data).To(Equal([]byte("foo")))
|
||||
Eventually(sent).Should(Receive())
|
||||
Expect(queue.Peek()).To(Equal(f))
|
||||
Expect(queue.Peek()).To(Equal(f))
|
||||
queue.Pop()
|
||||
Eventually(func() *wire.DatagramFrame { f = queue.Peek(); return f }).ShouldNot(BeNil())
|
||||
f = queue.Peek()
|
||||
Expect(f.Data).To(Equal([]byte("bar")))
|
||||
})
|
||||
|
||||
It("closes", func() {
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
errChan <- queue.AddAndWait(&wire.DatagramFrame{Data: []byte("foobar")})
|
||||
}()
|
||||
|
||||
Consistently(errChan).ShouldNot(Receive())
|
||||
queue.CloseWithError(errors.New("test error"))
|
||||
Eventually(errChan).Should(Receive(MatchError("test error")))
|
||||
})
|
||||
})
|
||||
|
||||
Context("receiving", func() {
|
||||
It("receives DATAGRAM frames", func() {
|
||||
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("foo")})
|
||||
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("bar")})
|
||||
data, err := queue.Receive(context.Background())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(data).To(Equal([]byte("foo")))
|
||||
data, err = queue.Receive(context.Background())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(data).To(Equal([]byte("bar")))
|
||||
})
|
||||
|
||||
It("blocks until a frame is received", func() {
|
||||
c := make(chan []byte, 1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
data, err := queue.Receive(context.Background())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
c <- data
|
||||
}()
|
||||
|
||||
Consistently(c).ShouldNot(Receive())
|
||||
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("foobar")})
|
||||
Eventually(c).Should(Receive(Equal([]byte("foobar"))))
|
||||
})
|
||||
|
||||
It("blocks until context is done", func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
_, err := queue.Receive(ctx)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
Consistently(errChan).ShouldNot(Receive())
|
||||
cancel()
|
||||
Eventually(errChan).Should(Receive(Equal(context.Canceled)))
|
||||
})
|
||||
|
||||
It("closes", func() {
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
_, err := queue.Receive(context.Background())
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
Consistently(errChan).ShouldNot(Receive())
|
||||
queue.CloseWithError(errors.New("test error"))
|
||||
Eventually(errChan).Should(Receive(MatchError("test error")))
|
||||
})
|
||||
})
|
||||
})
|
BIN
docs/quic.png
Normal file
BIN
docs/quic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
docs/quic.sketch
Normal file
BIN
docs/quic.sketch
Normal file
Binary file not shown.
65
docs/quic.svg
Normal file
65
docs/quic.svg
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="601px" height="242px" viewBox="0 0 601 242" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>quic</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-4-Copy" transform="translate(586.087964, 15.755663) rotate(-26.000000) translate(-586.087964, -15.755663) translate(573.036290, 1.253803)">
|
||||
<path d="M12.0494985,28.9428124 C-12.9338918,21.9172827 5.64340536,-9.86582259 25.705693,3.20698278 L12.0494985,28.9428124 L12.0494985,28.9428124 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#6AD7E5" transform="translate(12.861962, 14.514517) scale(-1, 1) translate(-12.861962, -14.514517) "></path>
|
||||
<path d="M12.1335889,20.1646293 C8.83752326,18.4210914 6.41944167,16.0531448 8.43561456,12.2408129 C10.3022111,8.71147453 13.769813,9.09473797 17.066293,10.8382759 L12.1335889,20.1646293 L12.1335889,20.1646293 Z" id="Shape" fill="#000000" transform="translate(12.382769, 14.854037) scale(-1, 1) translate(-12.382769, -14.854037) "></path>
|
||||
</g>
|
||||
<g id="Group-3-Copy" transform="translate(499.346306, 15.541651) rotate(25.000000) translate(-499.346306, -15.541651) translate(486.346306, 0.541651)">
|
||||
<path d="M0.262217125,3.72242032 C20.0373679,-10.1674613 37.9073886,21.170644 14.9248407,29.0555125 L0.262217125,3.72242032 L0.262217125,3.72242032 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#6AD7E5" transform="translate(12.997366, 14.636187) scale(-1, 1) translate(-12.997366, -14.636187) "></path>
|
||||
<path d="M16.7752477,19.7502904 C20.0713133,18.0067525 22.4893949,15.6388059 20.473222,11.8264741 C18.6066255,8.29713567 15.1390235,8.68039911 11.8425436,10.423937 L16.7752477,19.7502904 L16.7752477,19.7502904 Z" id="Shape" fill="#000000" transform="translate(16.526068, 14.439699) scale(-1, 1) translate(-16.526068, -14.439699) "></path>
|
||||
</g>
|
||||
<g id="Group-3" transform="translate(1.896652, 21.000000)">
|
||||
<path d="M0.262217125,3.72242032 C20.0373679,-10.1674613 37.9073886,21.170644 14.9248407,29.0555125 L0.262217125,3.72242032 L0.262217125,3.72242032 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#6AD7E5" transform="translate(12.997366, 14.636187) scale(-1, 1) translate(-12.997366, -14.636187) "></path>
|
||||
<path d="M16.7752477,19.7502904 C20.0713133,18.0067525 22.4893949,15.6388059 20.473222,11.8264741 C18.6066255,8.29713567 15.1390235,8.68039911 11.8425436,10.423937 L16.7752477,19.7502904 L16.7752477,19.7502904 Z" id="Shape" fill="#000000" transform="translate(16.526068, 14.439699) scale(-1, 1) translate(-16.526068, -14.439699) "></path>
|
||||
</g>
|
||||
<g id="Group-4" transform="translate(117.000000, 22.000000)">
|
||||
<path d="M12.0494985,28.9428124 C-12.9338918,21.9172827 5.64340536,-9.86582259 25.705693,3.20698278 L12.0494985,28.9428124 L12.0494985,28.9428124 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#6AD7E5" transform="translate(12.861962, 14.514517) scale(-1, 1) translate(-12.861962, -14.514517) "></path>
|
||||
<path d="M12.1335889,20.1646293 C8.83752326,18.4210914 6.41944167,16.0531448 8.43561456,12.2408129 C10.3022111,8.71147453 13.769813,9.09473797 17.066293,10.8382759 L12.1335889,20.1646293 L12.1335889,20.1646293 Z" id="Shape" fill="#000000" transform="translate(12.382769, 14.854037) scale(-1, 1) translate(-12.382769, -14.854037) "></path>
|
||||
</g>
|
||||
<g id="Group-2" transform="translate(26.000000, 40.000000)">
|
||||
<path d="M0.189217386,22.0262538 C4.67982195,48.130845 47.3500951,41.2287883 41.2211947,14.9008684 C35.725404,-8.70815982 -1.30323119,-2.17320732 0.189217386,22.0262538" id="Shape" stroke="#000000" stroke-width="2.9081" fill="#FFFFFF" transform="translate(20.983709, 19.323749) scale(-1, 1) translate(-20.983709, -19.323749) "></path>
|
||||
<path d="M52.375353,26.2483668 C58.1955709,48.9748532 94.5815666,43.1562926 93.210105,20.3593686 C91.566837,-6.94349061 46.8107821,-1.67517201 52.375353,26.2483668" id="Shape" stroke="#000000" stroke-width="2.8214" fill="#FFFFFF" transform="translate(72.572290, 21.519163) scale(-1, 1) translate(-72.572290, -21.519163) "></path>
|
||||
<path d="M44.873555,53.3511003 C44.8926146,56.7449499 45.6446396,60.55521 45.0028287,64.1657589 C44.1364461,65.803226 42.4368281,65.9764197 40.9717259,66.6381188 C38.9456089,66.3203209 37.2418475,64.9898788 36.429329,63.0946929 C35.9093337,58.9736786 36.6232396,54.9835954 36.754585,50.8609237 L44.873555,53.3511003 L44.873555,53.3511003 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#FFFFFF" transform="translate(40.751798, 58.749521) scale(-1, 1) translate(-40.751798, -58.749521) "></path>
|
||||
<g id="Group" transform="translate(82.660603, 22.581468) scale(-1, 1) translate(-82.660603, -22.581468) translate(76.238350, 15.744877)">
|
||||
<ellipse id="Oval" fill="#000000" cx="6.34932869" cy="6.79681466" rx="6.14423095" ry="6.65511077"></ellipse>
|
||||
<ellipse id="Oval" fill="#FFFFFF" cx="9.1440443" cy="8.29879302" rx="1.44852865" ry="1.69133123"></ellipse>
|
||||
</g>
|
||||
<g id="Group" transform="translate(31.282584, 20.924112) scale(-1, 1) translate(-31.282584, -20.924112) translate(24.860332, 14.087521)">
|
||||
<ellipse id="Oval" fill="#000000" cx="6.45167039" cy="6.79681466" rx="6.04188925" ry="6.65511077"></ellipse>
|
||||
<ellipse id="Oval" fill="#FFFFFF" cx="9.19998004" cy="8.29879302" rx="1.424497" ry="1.69133123"></ellipse>
|
||||
</g>
|
||||
<path d="M45.9112876,52.98151 C43.2305151,59.4783433 47.4062222,72.4699383 54.6799409,62.8875235 C54.1599456,58.7665092 54.8738514,54.776426 55.0051969,50.6537543 L45.9112876,52.98151 L45.9112876,52.98151 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#FFFFFF" transform="translate(50.049883, 58.475140) scale(-1, 1) translate(-50.049883, -58.475140) "></path>
|
||||
<g id="Group" transform="translate(44.127089, 43.712750) scale(-1, 1) translate(-44.127089, -43.712750) translate(27.346365, 32.732770)">
|
||||
<path d="M7.63667953,7.73694953 C2.64016722,8.16288988 -1.44397093,14.1036805 1.15393372,18.8035261 C4.59418928,25.0285532 12.2731314,18.2528698 17.0558448,18.8876369 C22.5603366,19.0003371 27.0704151,24.7078549 31.4914107,19.9193407 C36.40837,14.593429 29.3745535,9.4063208 23.8771055,7.0872662 L7.63667953,7.73694953 L7.63667953,7.73694953 Z" id="Shape" stroke="#231F20" stroke-width="3" fill="#F6D2A2"></path>
|
||||
<path d="M7.00771314,7.47674473 C6.63770854,-1.1792084 23.1412397,-2.2614615 25.0902897,4.98408215 C27.0343676,12.2126379 7.82023164,13.891539 7.00771314,7.47674473 C6.35927282,2.3542734 7.00771314,7.47674473 7.00771314,7.47674473 L7.00771314,7.47674473 Z" id="Shape" fill="#000000"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Group-2-Copy" transform="translate(544.820291, 73.354278) scale(-1, 1) translate(-544.820291, -73.354278) translate(498.000000, 40.000000)">
|
||||
<path d="M0.189217386,22.0262538 C4.67982195,48.130845 47.3500951,41.2287883 41.2211947,14.9008684 C35.725404,-8.70815982 -1.30323119,-2.17320732 0.189217386,22.0262538" id="Shape" stroke="#000000" stroke-width="2.9081" fill="#FFFFFF" transform="translate(20.983709, 19.323749) scale(-1, 1) translate(-20.983709, -19.323749) "></path>
|
||||
<path d="M52.375353,26.2483668 C58.1955709,48.9748532 94.5815666,43.1562926 93.210105,20.3593686 C91.566837,-6.94349061 46.8107821,-1.67517201 52.375353,26.2483668" id="Shape" stroke="#000000" stroke-width="2.8214" fill="#FFFFFF" transform="translate(72.572290, 21.519163) scale(-1, 1) translate(-72.572290, -21.519163) "></path>
|
||||
<path d="M44.873555,53.3511003 C44.8926146,56.7449499 45.6446396,60.55521 45.0028287,64.1657589 C44.1364461,65.803226 42.4368281,65.9764197 40.9717259,66.6381188 C38.9456089,66.3203209 37.2418475,64.9898788 36.429329,63.0946929 C35.9093337,58.9736786 36.6232396,54.9835954 36.754585,50.8609237 L44.873555,53.3511003 L44.873555,53.3511003 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#FFFFFF" transform="translate(40.751798, 58.749521) scale(-1, 1) translate(-40.751798, -58.749521) "></path>
|
||||
<g id="Group" transform="translate(82.660603, 22.581468) scale(-1, 1) translate(-82.660603, -22.581468) translate(76.238350, 15.744877)">
|
||||
<ellipse id="Oval" fill="#000000" cx="6.34932869" cy="6.79681466" rx="6.14423095" ry="6.65511077"></ellipse>
|
||||
<ellipse id="Oval" fill="#FFFFFF" cx="9.1440443" cy="8.29879302" rx="1.44852865" ry="1.69133123"></ellipse>
|
||||
</g>
|
||||
<g id="Group" transform="translate(31.282584, 20.924112) scale(-1, 1) translate(-31.282584, -20.924112) translate(24.860332, 14.087521)">
|
||||
<ellipse id="Oval" fill="#000000" cx="6.45167039" cy="6.79681466" rx="6.04188925" ry="6.65511077"></ellipse>
|
||||
<ellipse id="Oval" fill="#FFFFFF" cx="9.19998004" cy="8.29879302" rx="1.424497" ry="1.69133123"></ellipse>
|
||||
</g>
|
||||
<path d="M45.9112876,52.98151 C43.2305151,59.4783433 47.4062222,72.4699383 54.6799409,62.8875235 C54.1599456,58.7665092 54.8738514,54.776426 55.0051969,50.6537543 L45.9112876,52.98151 L45.9112876,52.98151 Z" id="Shape" stroke="#000000" stroke-width="3" fill="#FFFFFF" transform="translate(50.049883, 58.475140) scale(-1, 1) translate(-50.049883, -58.475140) "></path>
|
||||
<g id="Group" transform="translate(44.127089, 43.712750) scale(-1, 1) translate(-44.127089, -43.712750) translate(27.346365, 32.732770)">
|
||||
<path d="M7.63667953,7.73694953 C2.64016722,8.16288988 -1.44397093,14.1036805 1.15393372,18.8035261 C4.59418928,25.0285532 12.2731314,18.2528698 17.0558448,18.8876369 C22.5603366,19.0003371 27.0704151,24.7078549 31.4914107,19.9193407 C36.40837,14.593429 29.3745535,9.4063208 23.8771055,7.0872662 L7.63667953,7.73694953 L7.63667953,7.73694953 Z" id="Shape" stroke="#231F20" stroke-width="3" fill="#F6D2A2"></path>
|
||||
<path d="M7.00771314,7.47674473 C6.63770854,-1.1792084 23.1412397,-2.2614615 25.0902897,4.98408215 C27.0343676,12.2126379 7.82023164,13.891539 7.00771314,7.47674473 C6.35927282,2.3542734 7.00771314,7.47674473 7.00771314,7.47674473 L7.00771314,7.47674473 Z" id="Shape" fill="#000000"></path>
|
||||
</g>
|
||||
</g>
|
||||
<text id="QUIC" font-family="SourceCodePro-Light, Source Code Pro" font-size="288" font-weight="300" letter-spacing="-20" fill="#000000">
|
||||
<tspan x="-13.6" y="197">QUI</tspan>
|
||||
<tspan x="444.8" y="197">C</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
63
errors.go
Normal file
63
errors.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
)
|
||||
|
||||
type (
|
||||
TransportError = qerr.TransportError
|
||||
ApplicationError = qerr.ApplicationError
|
||||
VersionNegotiationError = qerr.VersionNegotiationError
|
||||
StatelessResetError = qerr.StatelessResetError
|
||||
IdleTimeoutError = qerr.IdleTimeoutError
|
||||
HandshakeTimeoutError = qerr.HandshakeTimeoutError
|
||||
)
|
||||
|
||||
type (
|
||||
TransportErrorCode = qerr.TransportErrorCode
|
||||
ApplicationErrorCode = qerr.ApplicationErrorCode
|
||||
StreamErrorCode = qerr.StreamErrorCode
|
||||
)
|
||||
|
||||
const (
|
||||
NoError = qerr.NoError
|
||||
InternalError = qerr.InternalError
|
||||
ConnectionRefused = qerr.ConnectionRefused
|
||||
FlowControlError = qerr.FlowControlError
|
||||
StreamLimitError = qerr.StreamLimitError
|
||||
StreamStateError = qerr.StreamStateError
|
||||
FinalSizeError = qerr.FinalSizeError
|
||||
FrameEncodingError = qerr.FrameEncodingError
|
||||
TransportParameterError = qerr.TransportParameterError
|
||||
ConnectionIDLimitError = qerr.ConnectionIDLimitError
|
||||
ProtocolViolation = qerr.ProtocolViolation
|
||||
InvalidToken = qerr.InvalidToken
|
||||
ApplicationErrorErrorCode = qerr.ApplicationErrorErrorCode
|
||||
CryptoBufferExceeded = qerr.CryptoBufferExceeded
|
||||
KeyUpdateError = qerr.KeyUpdateError
|
||||
AEADLimitReached = qerr.AEADLimitReached
|
||||
NoViablePathError = qerr.NoViablePathError
|
||||
)
|
||||
|
||||
// A StreamError is used for Stream.CancelRead and Stream.CancelWrite.
|
||||
// It is also returned from Stream.Read and Stream.Write if the peer canceled reading or writing.
|
||||
type StreamError struct {
|
||||
StreamID StreamID
|
||||
ErrorCode StreamErrorCode
|
||||
Remote bool
|
||||
}
|
||||
|
||||
func (e *StreamError) Is(target error) bool {
|
||||
_, ok := target.(*StreamError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e *StreamError) Error() string {
|
||||
pers := "local"
|
||||
if e.Remote {
|
||||
pers = "remote"
|
||||
}
|
||||
return fmt.Sprintf("stream %d canceled by %s with error code %d", e.StreamID, pers, e.ErrorCode)
|
||||
}
|
9
example/Dockerfile
Normal file
9
example/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
FROM scratch
|
||||
|
||||
VOLUME /certs
|
||||
VOLUME /www
|
||||
EXPOSE 6121
|
||||
|
||||
ADD main /main
|
||||
|
||||
CMD ["/main", "-bind=0.0.0.0", "-certpath=/certs/", "-www=/www"]
|
111
example/client/main.go
Normal file
111
example/client/main.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/quic-go/internal/testdata"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("v", false, "verbose")
|
||||
quiet := flag.Bool("q", false, "don't print the data")
|
||||
keyLogFile := flag.String("keylog", "", "key log file")
|
||||
insecure := flag.Bool("insecure", false, "skip certificate verification")
|
||||
enableQlog := flag.Bool("qlog", false, "output a qlog (in the same directory)")
|
||||
flag.Parse()
|
||||
urls := flag.Args()
|
||||
|
||||
logger := utils.DefaultLogger
|
||||
|
||||
if *verbose {
|
||||
logger.SetLogLevel(utils.LogLevelDebug)
|
||||
} else {
|
||||
logger.SetLogLevel(utils.LogLevelInfo)
|
||||
}
|
||||
logger.SetLogTimeFormat("")
|
||||
|
||||
var keyLog io.Writer
|
||||
if len(*keyLogFile) > 0 {
|
||||
f, err := os.Create(*keyLogFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
keyLog = f
|
||||
}
|
||||
|
||||
pool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
testdata.AddRootCA(pool)
|
||||
|
||||
var qconf quic.Config
|
||||
if *enableQlog {
|
||||
qconf.Tracer = func(ctx context.Context, p logging.Perspective, connID quic.ConnectionID) logging.ConnectionTracer {
|
||||
filename := fmt.Sprintf("client_%x.qlog", connID)
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Creating qlog file %s.\n", filename)
|
||||
return qlog.NewConnectionTracer(utils.NewBufferedWriteCloser(bufio.NewWriter(f), f), p, connID)
|
||||
}
|
||||
}
|
||||
roundTripper := &http3.RoundTripper{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: pool,
|
||||
InsecureSkipVerify: *insecure,
|
||||
KeyLogWriter: keyLog,
|
||||
},
|
||||
QuicConfig: &qconf,
|
||||
}
|
||||
defer roundTripper.Close()
|
||||
hclient := &http.Client{
|
||||
Transport: roundTripper,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(urls))
|
||||
for _, addr := range urls {
|
||||
logger.Infof("GET %s", addr)
|
||||
go func(addr string) {
|
||||
rsp, err := hclient.Get(addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
logger.Infof("Got response for %s: %#v", addr, rsp)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
_, err = io.Copy(body, rsp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *quiet {
|
||||
logger.Infof("Response Body: %d bytes", body.Len())
|
||||
} else {
|
||||
logger.Infof("Response Body:")
|
||||
logger.Infof("%s", body.Bytes())
|
||||
}
|
||||
wg.Done()
|
||||
}(addr)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
114
example/echo/echo.go
Normal file
114
example/echo/echo.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
const addr = "localhost:4242"
|
||||
|
||||
const message = "foobar"
|
||||
|
||||
// We start a server echoing data on the first stream the client opens,
|
||||
// then connect with a client, send the message, and wait for its receipt.
|
||||
func main() {
|
||||
go func() { log.Fatal(echoServer()) }()
|
||||
|
||||
err := clientMain()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start a server that echos all data on the first stream opened by the client
|
||||
func echoServer() error {
|
||||
listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := listener.Accept(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Echo through the loggingWriter
|
||||
_, err = io.Copy(loggingWriter{stream}, stream)
|
||||
return err
|
||||
}
|
||||
|
||||
func clientMain() error {
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"quic-echo-example"},
|
||||
}
|
||||
conn, err := quic.DialAddr(context.Background(), addr, tlsConf, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stream, err := conn.OpenStreamSync(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Client: Sending '%s'\n", message)
|
||||
_, err = stream.Write([]byte(message))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, len(message))
|
||||
_, err = io.ReadFull(stream, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Client: Got '%s'\n", buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A wrapper for io.Writer that also logs the message.
|
||||
type loggingWriter struct{ io.Writer }
|
||||
|
||||
func (w loggingWriter) Write(b []byte) (int, error) {
|
||||
fmt.Printf("Server: Got '%s'\n", string(b))
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
// Setup a bare-bones TLS config for the server
|
||||
func generateTLSConfig() *tls.Config {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"quic-echo-example"},
|
||||
}
|
||||
}
|
201
example/main.go
Normal file
201
example/main.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/quic-go/internal/testdata"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/logging"
|
||||
"github.com/quic-go/quic-go/qlog"
|
||||
)
|
||||
|
||||
type binds []string
|
||||
|
||||
func (b binds) String() string {
|
||||
return strings.Join(b, ",")
|
||||
}
|
||||
|
||||
func (b *binds) Set(v string) error {
|
||||
*b = strings.Split(v, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size is needed by the /demo/upload handler to determine the size of the uploaded file
|
||||
type Size interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
// See https://en.wikipedia.org/wiki/Lehmer_random_number_generator
|
||||
func generatePRData(l int) []byte {
|
||||
res := make([]byte, l)
|
||||
seed := uint64(1)
|
||||
for i := 0; i < l; i++ {
|
||||
seed = seed * 48271 % 2147483647
|
||||
res[i] = byte(seed)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func setupHandler(www string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
if len(www) > 0 {
|
||||
mux.Handle("/", http.FileServer(http.Dir(www)))
|
||||
} else {
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("%#v\n", r)
|
||||
const maxSize = 1 << 30 // 1 GB
|
||||
num, err := strconv.ParseInt(strings.ReplaceAll(r.RequestURI, "/", ""), 10, 64)
|
||||
if err != nil || num <= 0 || num > maxSize {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
w.Write(generatePRData(int(num)))
|
||||
})
|
||||
}
|
||||
|
||||
mux.HandleFunc("/demo/tile", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Small 40x40 png
|
||||
w.Write([]byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28,
|
||||
0x01, 0x03, 0x00, 0x00, 0x00, 0xb6, 0x30, 0x2a, 0x2e, 0x00, 0x00, 0x00,
|
||||
0x03, 0x50, 0x4c, 0x54, 0x45, 0x5a, 0xc3, 0x5a, 0xad, 0x38, 0xaa, 0xdb,
|
||||
0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0x63, 0x18,
|
||||
0x61, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x01, 0xe2, 0xb8, 0x75, 0x22, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
||||
})
|
||||
})
|
||||
|
||||
mux.HandleFunc("/demo/tiles", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "<html><head><style>img{width:40px;height:40px;}</style></head><body>")
|
||||
for i := 0; i < 200; i++ {
|
||||
fmt.Fprintf(w, `<img src="/demo/tile?cachebust=%d">`, i)
|
||||
}
|
||||
io.WriteString(w, "</body></html>")
|
||||
})
|
||||
|
||||
mux.HandleFunc("/demo/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("error reading body while handling /echo: %s\n", err.Error())
|
||||
}
|
||||
w.Write(body)
|
||||
})
|
||||
|
||||
// accept file uploads and return the MD5 of the uploaded file
|
||||
// maximum accepted file size is 1 GB
|
||||
mux.HandleFunc("/demo/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseMultipartForm(1 << 30) // 1 GB
|
||||
if err == nil {
|
||||
var file multipart.File
|
||||
file, _, err = r.FormFile("uploadfile")
|
||||
if err == nil {
|
||||
var size int64
|
||||
if sizeInterface, ok := file.(Size); ok {
|
||||
size = sizeInterface.Size()
|
||||
b := make([]byte, size)
|
||||
file.Read(b)
|
||||
md5 := md5.Sum(b)
|
||||
fmt.Fprintf(w, "%x", md5)
|
||||
return
|
||||
}
|
||||
err = errors.New("couldn't get uploaded file size")
|
||||
}
|
||||
}
|
||||
utils.DefaultLogger.Infof("Error receiving upload: %#v", err)
|
||||
}
|
||||
io.WriteString(w, `<html><body><form action="/demo/upload" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="uploadfile"><br>
|
||||
<input type="submit">
|
||||
</form></body></html>`)
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func main() {
|
||||
// defer profile.Start().Stop()
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
// runtime.SetBlockProfileRate(1)
|
||||
|
||||
verbose := flag.Bool("v", false, "verbose")
|
||||
bs := binds{}
|
||||
flag.Var(&bs, "bind", "bind to")
|
||||
www := flag.String("www", "", "www data")
|
||||
tcp := flag.Bool("tcp", false, "also listen on TCP")
|
||||
enableQlog := flag.Bool("qlog", false, "output a qlog (in the same directory)")
|
||||
flag.Parse()
|
||||
|
||||
logger := utils.DefaultLogger
|
||||
|
||||
if *verbose {
|
||||
logger.SetLogLevel(utils.LogLevelDebug)
|
||||
} else {
|
||||
logger.SetLogLevel(utils.LogLevelInfo)
|
||||
}
|
||||
logger.SetLogTimeFormat("")
|
||||
|
||||
if len(bs) == 0 {
|
||||
bs = binds{"localhost:6121"}
|
||||
}
|
||||
|
||||
handler := setupHandler(*www)
|
||||
quicConf := &quic.Config{}
|
||||
if *enableQlog {
|
||||
quicConf.Tracer = func(ctx context.Context, p logging.Perspective, connID quic.ConnectionID) logging.ConnectionTracer {
|
||||
filename := fmt.Sprintf("server_%x.qlog", connID)
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Creating qlog file %s.\n", filename)
|
||||
return qlog.NewConnectionTracer(utils.NewBufferedWriteCloser(bufio.NewWriter(f), f), p, connID)
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(bs))
|
||||
for _, b := range bs {
|
||||
bCap := b
|
||||
go func() {
|
||||
var err error
|
||||
if *tcp {
|
||||
certFile, keyFile := testdata.GetCertificatePaths()
|
||||
err = http3.ListenAndServe(bCap, certFile, keyFile, handler)
|
||||
} else {
|
||||
server := http3.Server{
|
||||
Handler: handler,
|
||||
Addr: bCap,
|
||||
QuicConfig: quicConf,
|
||||
}
|
||||
err = server.ListenAndServeTLS(testdata.GetCertificatePaths())
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
356
example/uquic/main.go
Normal file
356
example/uquic/main.go
Normal file
|
@ -0,0 +1,356 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
quic "github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
keyLogWriter, err := os.Create("./keylog.txt")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
ServerName: "quic.tlsfingerprint.io",
|
||||
// ServerName: "www.cloudflare.com",
|
||||
// MinVersion: tls.VersionTLS13,
|
||||
KeyLogWriter: keyLogWriter,
|
||||
// NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
quicConf := &quic.Config{}
|
||||
|
||||
roundTripper := &http3.RoundTripper{
|
||||
TLSClientConfig: tlsConf,
|
||||
QuicConfig: quicConf,
|
||||
}
|
||||
uRoundTripper := http3.GetURoundTripper(
|
||||
roundTripper,
|
||||
// getFFQUICSpec(),
|
||||
getCRQUICSpec(),
|
||||
nil,
|
||||
)
|
||||
defer uRoundTripper.Close()
|
||||
|
||||
hclient := &http.Client{
|
||||
Transport: uRoundTripper,
|
||||
}
|
||||
|
||||
addr := "https://quic.tlsfingerprint.io/qfp/?beautify=true"
|
||||
// addr := "https://www.cloudflare.com"
|
||||
|
||||
rsp, err := hclient.Get(addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Got response for %s: %#v", addr, rsp)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
_, err = io.Copy(body, rsp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Response Body: %s", body.Bytes())
|
||||
}
|
||||
|
||||
func getFFQUICSpec() *quic.QUICSpec {
|
||||
return &quic.QUICSpec{
|
||||
InitialPacketSpec: quic.InitialPacketSpec{
|
||||
SrcConnIDLength: 3,
|
||||
DestConnIDLength: 8,
|
||||
InitPacketNumberLength: 1,
|
||||
InitPacketNumber: 1,
|
||||
ClientTokenLength: 0,
|
||||
FrameOrder: quic.QUICFrames{
|
||||
&quic.QUICFrameCrypto{
|
||||
Offset: 300,
|
||||
Length: 0,
|
||||
},
|
||||
&quic.QUICFramePadding{
|
||||
Length: 125,
|
||||
},
|
||||
&quic.QUICFramePing{},
|
||||
&quic.QUICFrameCrypto{
|
||||
Offset: 0,
|
||||
Length: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientHelloSpec: getFFCHS(),
|
||||
}
|
||||
}
|
||||
|
||||
func getFFCHS() *tls.ClientHelloSpec {
|
||||
return &tls.ClientHelloSpec{
|
||||
TLSVersMin: tls.VersionTLS13,
|
||||
TLSVersMax: tls.VersionTLS13,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
},
|
||||
CompressionMethods: []uint8{
|
||||
0x0, // no compression
|
||||
},
|
||||
Extensions: []tls.TLSExtension{
|
||||
&tls.SNIExtension{},
|
||||
&tls.ExtendedMasterSecretExtension{},
|
||||
&tls.RenegotiationInfoExtension{
|
||||
Renegotiation: tls.RenegotiateOnceAsClient,
|
||||
},
|
||||
&tls.SupportedCurvesExtension{
|
||||
Curves: []tls.CurveID{
|
||||
tls.CurveX25519,
|
||||
tls.CurveSECP256R1,
|
||||
tls.CurveSECP384R1,
|
||||
tls.CurveSECP521R1,
|
||||
tls.FakeCurveFFDHE2048,
|
||||
tls.FakeCurveFFDHE3072,
|
||||
tls.FakeCurveFFDHE4096,
|
||||
tls.FakeCurveFFDHE6144,
|
||||
tls.FakeCurveFFDHE8192,
|
||||
},
|
||||
},
|
||||
&tls.ALPNExtension{
|
||||
AlpnProtocols: []string{
|
||||
"h3",
|
||||
},
|
||||
},
|
||||
&tls.StatusRequestExtension{},
|
||||
&tls.FakeDelegatedCredentialsExtension{
|
||||
SupportedSignatureAlgorithms: []tls.SignatureScheme{
|
||||
tls.ECDSAWithP256AndSHA256,
|
||||
tls.ECDSAWithP384AndSHA384,
|
||||
tls.ECDSAWithP521AndSHA512,
|
||||
tls.ECDSAWithSHA1,
|
||||
},
|
||||
},
|
||||
&tls.KeyShareExtension{
|
||||
KeyShares: []tls.KeyShare{
|
||||
{
|
||||
Group: tls.X25519,
|
||||
},
|
||||
// {
|
||||
// Group: tls.CurveP256,
|
||||
// },
|
||||
},
|
||||
},
|
||||
&tls.SupportedVersionsExtension{
|
||||
Versions: []uint16{
|
||||
tls.VersionTLS13,
|
||||
},
|
||||
},
|
||||
&tls.SignatureAlgorithmsExtension{
|
||||
SupportedSignatureAlgorithms: []tls.SignatureScheme{
|
||||
tls.ECDSAWithP256AndSHA256,
|
||||
tls.ECDSAWithP384AndSHA384,
|
||||
tls.ECDSAWithP521AndSHA512,
|
||||
tls.ECDSAWithSHA1,
|
||||
tls.PSSWithSHA256,
|
||||
tls.PSSWithSHA384,
|
||||
tls.PSSWithSHA512,
|
||||
tls.PKCS1WithSHA256,
|
||||
tls.PKCS1WithSHA384,
|
||||
tls.PKCS1WithSHA512,
|
||||
tls.PKCS1WithSHA1,
|
||||
},
|
||||
},
|
||||
&tls.PSKKeyExchangeModesExtension{
|
||||
Modes: []uint8{
|
||||
tls.PskModeDHE,
|
||||
},
|
||||
},
|
||||
&tls.FakeRecordSizeLimitExtension{
|
||||
Limit: 0x4001,
|
||||
},
|
||||
&tls.QUICTransportParametersExtension{
|
||||
TransportParameters: tls.TransportParameters{
|
||||
tls.InitialMaxStreamDataBidiRemote(0x100000),
|
||||
tls.InitialMaxStreamsBidi(16),
|
||||
tls.MaxDatagramFrameSize(1200),
|
||||
tls.MaxIdleTimeout(30000),
|
||||
tls.ActiveConnectionIDLimit(8),
|
||||
&tls.GREASEQUICBit{},
|
||||
&tls.VersionInformation{
|
||||
ChoosenVersion: tls.VERSION_1,
|
||||
AvailableVersions: []uint32{
|
||||
tls.VERSION_GREASE,
|
||||
tls.VERSION_1,
|
||||
},
|
||||
LegacyID: true,
|
||||
},
|
||||
tls.InitialMaxStreamsUni(16),
|
||||
&tls.GREASE{
|
||||
IdOverride: 0xff02de1a,
|
||||
ValueOverride: []byte{
|
||||
0x43, 0xe8,
|
||||
},
|
||||
},
|
||||
tls.InitialMaxStreamDataBidiLocal(0xc00000),
|
||||
tls.InitialMaxStreamDataUni(0x100000),
|
||||
tls.InitialSourceConnectionID([]byte{}),
|
||||
tls.MaxAckDelay(20),
|
||||
tls.InitialMaxData(0x1800000),
|
||||
&tls.DisableActiveMigration{},
|
||||
},
|
||||
},
|
||||
&tls.UtlsPaddingExtension{
|
||||
GetPaddingLen: tls.BoringPaddingStyle,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCRQUICSpec() *quic.QUICSpec {
|
||||
return &quic.QUICSpec{
|
||||
InitialPacketSpec: quic.InitialPacketSpec{
|
||||
SrcConnIDLength: 0,
|
||||
DestConnIDLength: 8,
|
||||
InitPacketNumberLength: 1,
|
||||
InitPacketNumber: 1,
|
||||
ClientTokenLength: 0,
|
||||
FrameOrder: quic.QUICFrames{
|
||||
&quic.QUICFrameCrypto{
|
||||
Offset: 300,
|
||||
Length: 0,
|
||||
},
|
||||
&quic.QUICFramePadding{
|
||||
Length: 125,
|
||||
},
|
||||
&quic.QUICFramePing{},
|
||||
&quic.QUICFrameCrypto{
|
||||
Offset: 0,
|
||||
Length: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientHelloSpec: getCRCHS(),
|
||||
}
|
||||
}
|
||||
func getCRCHS() *tls.ClientHelloSpec {
|
||||
return &tls.ClientHelloSpec{
|
||||
TLSVersMin: tls.VersionTLS13,
|
||||
TLSVersMax: tls.VersionTLS13,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
},
|
||||
CompressionMethods: []uint8{
|
||||
0x0, // no compression
|
||||
},
|
||||
Extensions: []tls.TLSExtension{
|
||||
&tls.SNIExtension{},
|
||||
&tls.ExtendedMasterSecretExtension{},
|
||||
&tls.RenegotiationInfoExtension{
|
||||
Renegotiation: tls.RenegotiateOnceAsClient,
|
||||
},
|
||||
&tls.SupportedCurvesExtension{
|
||||
Curves: []tls.CurveID{
|
||||
tls.CurveX25519,
|
||||
tls.CurveSECP256R1,
|
||||
tls.CurveSECP384R1,
|
||||
tls.CurveSECP521R1,
|
||||
tls.FakeCurveFFDHE2048,
|
||||
tls.FakeCurveFFDHE3072,
|
||||
tls.FakeCurveFFDHE4096,
|
||||
tls.FakeCurveFFDHE6144,
|
||||
tls.FakeCurveFFDHE8192,
|
||||
},
|
||||
},
|
||||
&tls.ALPNExtension{
|
||||
AlpnProtocols: []string{
|
||||
"h3",
|
||||
},
|
||||
},
|
||||
&tls.StatusRequestExtension{},
|
||||
&tls.FakeDelegatedCredentialsExtension{
|
||||
SupportedSignatureAlgorithms: []tls.SignatureScheme{
|
||||
tls.ECDSAWithP256AndSHA256,
|
||||
tls.ECDSAWithP384AndSHA384,
|
||||
tls.ECDSAWithP521AndSHA512,
|
||||
tls.ECDSAWithSHA1,
|
||||
},
|
||||
},
|
||||
&tls.KeyShareExtension{
|
||||
KeyShares: []tls.KeyShare{
|
||||
{
|
||||
Group: tls.X25519,
|
||||
},
|
||||
// {
|
||||
// Group: tls.CurveP256,
|
||||
// },
|
||||
},
|
||||
},
|
||||
&tls.SupportedVersionsExtension{
|
||||
Versions: []uint16{
|
||||
tls.VersionTLS13,
|
||||
},
|
||||
},
|
||||
&tls.SignatureAlgorithmsExtension{
|
||||
SupportedSignatureAlgorithms: []tls.SignatureScheme{
|
||||
tls.ECDSAWithP256AndSHA256,
|
||||
tls.ECDSAWithP384AndSHA384,
|
||||
tls.ECDSAWithP521AndSHA512,
|
||||
tls.ECDSAWithSHA1,
|
||||
tls.PSSWithSHA256,
|
||||
tls.PSSWithSHA384,
|
||||
tls.PSSWithSHA512,
|
||||
tls.PKCS1WithSHA256,
|
||||
tls.PKCS1WithSHA384,
|
||||
tls.PKCS1WithSHA512,
|
||||
tls.PKCS1WithSHA1,
|
||||
},
|
||||
},
|
||||
&tls.PSKKeyExchangeModesExtension{
|
||||
Modes: []uint8{
|
||||
tls.PskModeDHE,
|
||||
},
|
||||
},
|
||||
&tls.FakeRecordSizeLimitExtension{
|
||||
Limit: 0x4001,
|
||||
},
|
||||
&tls.QUICTransportParametersExtension{
|
||||
TransportParameters: tls.TransportParameters{
|
||||
&tls.GREASE{
|
||||
IdOverride: 0x35967c5b9c37e023,
|
||||
ValueOverride: []byte{
|
||||
0xfc, 0x97, 0xbb, 0x57, 0xb8, 0x02, 0x19, 0xcd,
|
||||
},
|
||||
},
|
||||
tls.InitialMaxStreamsUni(103),
|
||||
tls.InitialSourceConnectionID([]byte{}),
|
||||
tls.InitialMaxStreamsBidi(100),
|
||||
tls.InitialMaxData(15728640),
|
||||
&tls.VersionInformation{
|
||||
ChoosenVersion: tls.VERSION_1,
|
||||
AvailableVersions: []uint32{
|
||||
tls.VERSION_1,
|
||||
tls.VERSION_GREASE,
|
||||
},
|
||||
LegacyID: true,
|
||||
},
|
||||
tls.MaxIdleTimeout(30000),
|
||||
tls.MaxUDPPayloadSize(1472),
|
||||
tls.MaxDatagramFrameSize(65536),
|
||||
tls.InitialMaxStreamDataBidiLocal(6291456),
|
||||
tls.InitialMaxStreamDataUni(6291456),
|
||||
tls.InitialMaxStreamDataBidiRemote(6291456),
|
||||
},
|
||||
},
|
||||
&tls.UtlsPaddingExtension{
|
||||
GetPaddingLen: tls.BoringPaddingStyle,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
237
frame_sorter.go
Normal file
237
frame_sorter.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
list "github.com/quic-go/quic-go/internal/utils/linkedlist"
|
||||
)
|
||||
|
||||
// byteInterval is an interval from one ByteCount to the other
|
||||
type byteInterval struct {
|
||||
Start protocol.ByteCount
|
||||
End protocol.ByteCount
|
||||
}
|
||||
|
||||
var byteIntervalElementPool sync.Pool
|
||||
|
||||
func init() {
|
||||
byteIntervalElementPool = *list.NewPool[byteInterval]()
|
||||
}
|
||||
|
||||
type frameSorterEntry struct {
|
||||
Data []byte
|
||||
DoneCb func()
|
||||
}
|
||||
|
||||
type frameSorter struct {
|
||||
queue map[protocol.ByteCount]frameSorterEntry
|
||||
readPos protocol.ByteCount
|
||||
gaps *list.List[byteInterval]
|
||||
}
|
||||
|
||||
var errDuplicateStreamData = errors.New("duplicate stream data")
|
||||
|
||||
func newFrameSorter() *frameSorter {
|
||||
s := frameSorter{
|
||||
gaps: list.NewWithPool[byteInterval](&byteIntervalElementPool),
|
||||
queue: make(map[protocol.ByteCount]frameSorterEntry),
|
||||
}
|
||||
s.gaps.PushFront(byteInterval{Start: 0, End: protocol.MaxByteCount})
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *frameSorter) Push(data []byte, offset protocol.ByteCount, doneCb func()) error {
|
||||
err := s.push(data, offset, doneCb)
|
||||
if err == errDuplicateStreamData {
|
||||
if doneCb != nil {
|
||||
doneCb()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *frameSorter) push(data []byte, offset protocol.ByteCount, doneCb func()) error {
|
||||
if len(data) == 0 {
|
||||
return errDuplicateStreamData
|
||||
}
|
||||
|
||||
start := offset
|
||||
end := offset + protocol.ByteCount(len(data))
|
||||
|
||||
if end <= s.gaps.Front().Value.Start {
|
||||
return errDuplicateStreamData
|
||||
}
|
||||
|
||||
startGap, startsInGap := s.findStartGap(start)
|
||||
endGap, endsInGap := s.findEndGap(startGap, end)
|
||||
|
||||
startGapEqualsEndGap := startGap == endGap
|
||||
|
||||
if (startGapEqualsEndGap && end <= startGap.Value.Start) ||
|
||||
(!startGapEqualsEndGap && startGap.Value.End >= endGap.Value.Start && end <= startGap.Value.Start) {
|
||||
return errDuplicateStreamData
|
||||
}
|
||||
|
||||
startGapNext := startGap.Next()
|
||||
startGapEnd := startGap.Value.End // save it, in case startGap is modified
|
||||
endGapStart := endGap.Value.Start // save it, in case endGap is modified
|
||||
endGapEnd := endGap.Value.End // save it, in case endGap is modified
|
||||
var adjustedStartGapEnd bool
|
||||
var wasCut bool
|
||||
|
||||
pos := start
|
||||
var hasReplacedAtLeastOne bool
|
||||
for {
|
||||
oldEntry, ok := s.queue[pos]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
oldEntryLen := protocol.ByteCount(len(oldEntry.Data))
|
||||
if end-pos > oldEntryLen || (hasReplacedAtLeastOne && end-pos == oldEntryLen) {
|
||||
// The existing frame is shorter than the new frame. Replace it.
|
||||
delete(s.queue, pos)
|
||||
pos += oldEntryLen
|
||||
hasReplacedAtLeastOne = true
|
||||
if oldEntry.DoneCb != nil {
|
||||
oldEntry.DoneCb()
|
||||
}
|
||||
} else {
|
||||
if !hasReplacedAtLeastOne {
|
||||
return errDuplicateStreamData
|
||||
}
|
||||
// The existing frame is longer than the new frame.
|
||||
// Cut the new frame such that the end aligns with the start of the existing frame.
|
||||
data = data[:pos-start]
|
||||
end = pos
|
||||
wasCut = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !startsInGap && !hasReplacedAtLeastOne {
|
||||
// cut the frame, such that it starts at the start of the gap
|
||||
data = data[startGap.Value.Start-start:]
|
||||
start = startGap.Value.Start
|
||||
wasCut = true
|
||||
}
|
||||
if start <= startGap.Value.Start {
|
||||
if end >= startGap.Value.End {
|
||||
// The frame covers the whole startGap. Delete the gap.
|
||||
s.gaps.Remove(startGap)
|
||||
} else {
|
||||
startGap.Value.Start = end
|
||||
}
|
||||
} else if !hasReplacedAtLeastOne {
|
||||
startGap.Value.End = start
|
||||
adjustedStartGapEnd = true
|
||||
}
|
||||
|
||||
if !startGapEqualsEndGap {
|
||||
s.deleteConsecutive(startGapEnd)
|
||||
var nextGap *list.Element[byteInterval]
|
||||
for gap := startGapNext; gap.Value.End < endGapStart; gap = nextGap {
|
||||
nextGap = gap.Next()
|
||||
s.deleteConsecutive(gap.Value.End)
|
||||
s.gaps.Remove(gap)
|
||||
}
|
||||
}
|
||||
|
||||
if !endsInGap && start != endGapEnd && end > endGapEnd {
|
||||
// cut the frame, such that it ends at the end of the gap
|
||||
data = data[:endGapEnd-start]
|
||||
end = endGapEnd
|
||||
wasCut = true
|
||||
}
|
||||
if end == endGapEnd {
|
||||
if !startGapEqualsEndGap {
|
||||
// The frame covers the whole endGap. Delete the gap.
|
||||
s.gaps.Remove(endGap)
|
||||
}
|
||||
} else {
|
||||
if startGapEqualsEndGap && adjustedStartGapEnd {
|
||||
// The frame split the existing gap into two.
|
||||
s.gaps.InsertAfter(byteInterval{Start: end, End: startGapEnd}, startGap)
|
||||
} else if !startGapEqualsEndGap {
|
||||
endGap.Value.Start = end
|
||||
}
|
||||
}
|
||||
|
||||
if wasCut && len(data) < protocol.MinStreamFrameBufferSize {
|
||||
newData := make([]byte, len(data))
|
||||
copy(newData, data)
|
||||
data = newData
|
||||
if doneCb != nil {
|
||||
doneCb()
|
||||
doneCb = nil
|
||||
}
|
||||
}
|
||||
|
||||
if s.gaps.Len() > protocol.MaxStreamFrameSorterGaps {
|
||||
return errors.New("too many gaps in received data")
|
||||
}
|
||||
|
||||
s.queue[start] = frameSorterEntry{Data: data, DoneCb: doneCb}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *frameSorter) findStartGap(offset protocol.ByteCount) (*list.Element[byteInterval], bool) {
|
||||
for gap := s.gaps.Front(); gap != nil; gap = gap.Next() {
|
||||
if offset >= gap.Value.Start && offset <= gap.Value.End {
|
||||
return gap, true
|
||||
}
|
||||
if offset < gap.Value.Start {
|
||||
return gap, false
|
||||
}
|
||||
}
|
||||
panic("no gap found")
|
||||
}
|
||||
|
||||
func (s *frameSorter) findEndGap(startGap *list.Element[byteInterval], offset protocol.ByteCount) (*list.Element[byteInterval], bool) {
|
||||
for gap := startGap; gap != nil; gap = gap.Next() {
|
||||
if offset >= gap.Value.Start && offset < gap.Value.End {
|
||||
return gap, true
|
||||
}
|
||||
if offset < gap.Value.Start {
|
||||
return gap.Prev(), false
|
||||
}
|
||||
}
|
||||
panic("no gap found")
|
||||
}
|
||||
|
||||
// deleteConsecutive deletes consecutive frames from the queue, starting at pos
|
||||
func (s *frameSorter) deleteConsecutive(pos protocol.ByteCount) {
|
||||
for {
|
||||
oldEntry, ok := s.queue[pos]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
oldEntryLen := protocol.ByteCount(len(oldEntry.Data))
|
||||
delete(s.queue, pos)
|
||||
if oldEntry.DoneCb != nil {
|
||||
oldEntry.DoneCb()
|
||||
}
|
||||
pos += oldEntryLen
|
||||
}
|
||||
}
|
||||
|
||||
func (s *frameSorter) Pop() (protocol.ByteCount, []byte, func()) {
|
||||
entry, ok := s.queue[s.readPos]
|
||||
if !ok {
|
||||
return s.readPos, nil, nil
|
||||
}
|
||||
delete(s.queue, s.readPos)
|
||||
offset := s.readPos
|
||||
s.readPos += protocol.ByteCount(len(entry.Data))
|
||||
if s.gaps.Front().Value.End <= s.readPos {
|
||||
panic("frame sorter BUG: read position higher than a gap")
|
||||
}
|
||||
return offset, entry.Data, entry.DoneCb
|
||||
}
|
||||
|
||||
// HasMoreData says if there is any more data queued at *any* offset.
|
||||
func (s *frameSorter) HasMoreData() bool {
|
||||
return len(s.queue) > 0
|
||||
}
|
1528
frame_sorter_test.go
Normal file
1528
frame_sorter_test.go
Normal file
File diff suppressed because it is too large
Load diff
165
framer.go
Normal file
165
framer.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/ackhandler"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils/ringbuffer"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
type framer interface {
|
||||
HasData() bool
|
||||
|
||||
QueueControlFrame(wire.Frame)
|
||||
AppendControlFrames([]ackhandler.Frame, protocol.ByteCount, protocol.VersionNumber) ([]ackhandler.Frame, protocol.ByteCount)
|
||||
|
||||
AddActiveStream(protocol.StreamID)
|
||||
AppendStreamFrames([]ackhandler.StreamFrame, protocol.ByteCount, protocol.VersionNumber) ([]ackhandler.StreamFrame, protocol.ByteCount)
|
||||
|
||||
Handle0RTTRejection() error
|
||||
}
|
||||
|
||||
type framerI struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
streamGetter streamGetter
|
||||
|
||||
activeStreams map[protocol.StreamID]struct{}
|
||||
streamQueue ringbuffer.RingBuffer[protocol.StreamID]
|
||||
|
||||
controlFrameMutex sync.Mutex
|
||||
controlFrames []wire.Frame
|
||||
}
|
||||
|
||||
var _ framer = &framerI{}
|
||||
|
||||
func newFramer(streamGetter streamGetter) framer {
|
||||
return &framerI{
|
||||
streamGetter: streamGetter,
|
||||
activeStreams: make(map[protocol.StreamID]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *framerI) HasData() bool {
|
||||
f.mutex.Lock()
|
||||
hasData := !f.streamQueue.Empty()
|
||||
f.mutex.Unlock()
|
||||
if hasData {
|
||||
return true
|
||||
}
|
||||
f.controlFrameMutex.Lock()
|
||||
hasData = len(f.controlFrames) > 0
|
||||
f.controlFrameMutex.Unlock()
|
||||
return hasData
|
||||
}
|
||||
|
||||
func (f *framerI) QueueControlFrame(frame wire.Frame) {
|
||||
f.controlFrameMutex.Lock()
|
||||
f.controlFrames = append(f.controlFrames, frame)
|
||||
f.controlFrameMutex.Unlock()
|
||||
}
|
||||
|
||||
func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.ByteCount, v protocol.VersionNumber) ([]ackhandler.Frame, protocol.ByteCount) {
|
||||
var length protocol.ByteCount
|
||||
f.controlFrameMutex.Lock()
|
||||
for len(f.controlFrames) > 0 {
|
||||
frame := f.controlFrames[len(f.controlFrames)-1]
|
||||
frameLen := frame.Length(v)
|
||||
if length+frameLen > maxLen {
|
||||
break
|
||||
}
|
||||
frames = append(frames, ackhandler.Frame{Frame: frame})
|
||||
length += frameLen
|
||||
f.controlFrames = f.controlFrames[:len(f.controlFrames)-1]
|
||||
}
|
||||
f.controlFrameMutex.Unlock()
|
||||
return frames, length
|
||||
}
|
||||
|
||||
func (f *framerI) AddActiveStream(id protocol.StreamID) {
|
||||
f.mutex.Lock()
|
||||
if _, ok := f.activeStreams[id]; !ok {
|
||||
f.streamQueue.PushBack(id)
|
||||
f.activeStreams[id] = struct{}{}
|
||||
}
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (f *framerI) AppendStreamFrames(frames []ackhandler.StreamFrame, maxLen protocol.ByteCount, v protocol.VersionNumber) ([]ackhandler.StreamFrame, protocol.ByteCount) {
|
||||
startLen := len(frames)
|
||||
var length protocol.ByteCount
|
||||
f.mutex.Lock()
|
||||
// pop STREAM frames, until less than MinStreamFrameSize bytes are left in the packet
|
||||
numActiveStreams := f.streamQueue.Len()
|
||||
for i := 0; i < numActiveStreams; i++ {
|
||||
if protocol.MinStreamFrameSize+length > maxLen {
|
||||
break
|
||||
}
|
||||
id := f.streamQueue.PopFront()
|
||||
// This should never return an error. Better check it anyway.
|
||||
// The stream will only be in the streamQueue, if it enqueued itself there.
|
||||
str, err := f.streamGetter.GetOrOpenSendStream(id)
|
||||
// The stream can be nil if it completed after it said it had data.
|
||||
if str == nil || err != nil {
|
||||
delete(f.activeStreams, id)
|
||||
continue
|
||||
}
|
||||
remainingLen := maxLen - length
|
||||
// For the last STREAM frame, we'll remove the DataLen field later.
|
||||
// Therefore, we can pretend to have more bytes available when popping
|
||||
// the STREAM frame (which will always have the DataLen set).
|
||||
remainingLen += quicvarint.Len(uint64(remainingLen))
|
||||
frame, ok, hasMoreData := str.popStreamFrame(remainingLen, v)
|
||||
if hasMoreData { // put the stream back in the queue (at the end)
|
||||
f.streamQueue.PushBack(id)
|
||||
} else { // no more data to send. Stream is not active
|
||||
delete(f.activeStreams, id)
|
||||
}
|
||||
// The frame can be "nil"
|
||||
// * if the receiveStream was canceled after it said it had data
|
||||
// * the remaining size doesn't allow us to add another STREAM frame
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
frames = append(frames, frame)
|
||||
length += frame.Frame.Length(v)
|
||||
}
|
||||
f.mutex.Unlock()
|
||||
if len(frames) > startLen {
|
||||
l := frames[len(frames)-1].Frame.Length(v)
|
||||
// account for the smaller size of the last STREAM frame
|
||||
frames[len(frames)-1].Frame.DataLenPresent = false
|
||||
length += frames[len(frames)-1].Frame.Length(v) - l
|
||||
}
|
||||
return frames, length
|
||||
}
|
||||
|
||||
func (f *framerI) Handle0RTTRejection() error {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
f.controlFrameMutex.Lock()
|
||||
f.streamQueue.Clear()
|
||||
for id := range f.activeStreams {
|
||||
delete(f.activeStreams, id)
|
||||
}
|
||||
var j int
|
||||
for i, frame := range f.controlFrames {
|
||||
switch frame.(type) {
|
||||
case *wire.MaxDataFrame, *wire.MaxStreamDataFrame, *wire.MaxStreamsFrame:
|
||||
return errors.New("didn't expect MAX_DATA / MAX_STREAM_DATA / MAX_STREAMS frame to be sent in 0-RTT")
|
||||
case *wire.DataBlockedFrame, *wire.StreamDataBlockedFrame, *wire.StreamsBlockedFrame:
|
||||
continue
|
||||
default:
|
||||
f.controlFrames[j] = f.controlFrames[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
f.controlFrames = f.controlFrames[:j]
|
||||
f.controlFrameMutex.Unlock()
|
||||
return nil
|
||||
}
|
388
framer_test.go
Normal file
388
framer_test.go
Normal file
|
@ -0,0 +1,388 @@
|
|||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/ackhandler"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Framer", func() {
|
||||
const (
|
||||
id1 = protocol.StreamID(10)
|
||||
id2 = protocol.StreamID(11)
|
||||
)
|
||||
|
||||
var (
|
||||
framer framer
|
||||
stream1, stream2 *MockSendStreamI
|
||||
streamGetter *MockStreamGetter
|
||||
version protocol.VersionNumber
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
streamGetter = NewMockStreamGetter(mockCtrl)
|
||||
stream1 = NewMockSendStreamI(mockCtrl)
|
||||
stream1.EXPECT().StreamID().Return(protocol.StreamID(5)).AnyTimes()
|
||||
stream2 = NewMockSendStreamI(mockCtrl)
|
||||
stream2.EXPECT().StreamID().Return(protocol.StreamID(6)).AnyTimes()
|
||||
framer = newFramer(streamGetter)
|
||||
})
|
||||
|
||||
Context("handling control frames", func() {
|
||||
It("adds control frames", func() {
|
||||
mdf := &wire.MaxDataFrame{MaximumData: 0x42}
|
||||
msf := &wire.MaxStreamsFrame{MaxStreamNum: 0x1337}
|
||||
framer.QueueControlFrame(mdf)
|
||||
framer.QueueControlFrame(msf)
|
||||
frames, length := framer.AppendControlFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(2))
|
||||
fs := []wire.Frame{frames[0].Frame, frames[1].Frame}
|
||||
Expect(fs).To(ContainElement(mdf))
|
||||
Expect(fs).To(ContainElement(msf))
|
||||
Expect(length).To(Equal(mdf.Length(version) + msf.Length(version)))
|
||||
})
|
||||
|
||||
It("says if it has data", func() {
|
||||
Expect(framer.HasData()).To(BeFalse())
|
||||
f := &wire.MaxDataFrame{MaximumData: 0x42}
|
||||
framer.QueueControlFrame(f)
|
||||
Expect(framer.HasData()).To(BeTrue())
|
||||
frames, _ := framer.AppendControlFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(framer.HasData()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("appends to the slice given", func() {
|
||||
ping := &wire.PingFrame{}
|
||||
mdf := &wire.MaxDataFrame{MaximumData: 0x42}
|
||||
framer.QueueControlFrame(mdf)
|
||||
frames, length := framer.AppendControlFrames([]ackhandler.Frame{{Frame: ping}}, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(2))
|
||||
Expect(frames[0].Frame).To(Equal(ping))
|
||||
Expect(frames[1].Frame).To(Equal(mdf))
|
||||
Expect(length).To(Equal(mdf.Length(version)))
|
||||
})
|
||||
|
||||
It("adds the right number of frames", func() {
|
||||
maxSize := protocol.ByteCount(1000)
|
||||
bf := &wire.DataBlockedFrame{MaximumData: 0x1337}
|
||||
bfLen := bf.Length(version)
|
||||
numFrames := int(maxSize / bfLen) // max number of frames that fit into maxSize
|
||||
for i := 0; i < numFrames+1; i++ {
|
||||
framer.QueueControlFrame(bf)
|
||||
}
|
||||
frames, length := framer.AppendControlFrames(nil, maxSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(numFrames))
|
||||
Expect(length).To(BeNumerically(">", maxSize-bfLen))
|
||||
frames, length = framer.AppendControlFrames(nil, maxSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(length).To(Equal(bfLen))
|
||||
})
|
||||
|
||||
It("drops *_BLOCKED frames when 0-RTT is rejected", func() {
|
||||
ping := &wire.PingFrame{}
|
||||
ncid := &wire.NewConnectionIDFrame{
|
||||
SequenceNumber: 10,
|
||||
ConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
|
||||
}
|
||||
frames := []wire.Frame{
|
||||
&wire.DataBlockedFrame{MaximumData: 1337},
|
||||
&wire.StreamDataBlockedFrame{StreamID: 42, MaximumStreamData: 1337},
|
||||
&wire.StreamsBlockedFrame{StreamLimit: 13},
|
||||
ping,
|
||||
ncid,
|
||||
}
|
||||
rand.Shuffle(len(frames), func(i, j int) { frames[i], frames[j] = frames[j], frames[i] })
|
||||
for _, f := range frames {
|
||||
framer.QueueControlFrame(f)
|
||||
}
|
||||
Expect(framer.Handle0RTTRejection()).To(Succeed())
|
||||
fs, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
|
||||
Expect(fs).To(HaveLen(2))
|
||||
Expect(length).To(Equal(ping.Length(version) + ncid.Length(version)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("popping STREAM frames", func() {
|
||||
It("returns nil when popping an empty framer", func() {
|
||||
Expect(framer.AppendStreamFrames(nil, 1000, protocol.Version1)).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("returns STREAM frames", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id1,
|
||||
Data: []byte("foobar"),
|
||||
Offset: 42,
|
||||
DataLenPresent: true,
|
||||
}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
fs, length := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(fs).To(HaveLen(1))
|
||||
Expect(fs[0].Frame.DataLenPresent).To(BeFalse())
|
||||
Expect(length).To(Equal(f.Length(version)))
|
||||
})
|
||||
|
||||
It("says if it has data", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil).Times(2)
|
||||
Expect(framer.HasData()).To(BeFalse())
|
||||
framer.AddActiveStream(id1)
|
||||
Expect(framer.HasData()).To(BeTrue())
|
||||
f1 := &wire.StreamFrame{StreamID: id1, Data: []byte("foo")}
|
||||
f2 := &wire.StreamFrame{StreamID: id1, Data: []byte("bar")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f1}, true, true)
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f2}, true, false)
|
||||
frames, _ := framer.AppendStreamFrames(nil, protocol.MaxByteCount, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f1))
|
||||
Expect(framer.HasData()).To(BeTrue())
|
||||
frames, _ = framer.AppendStreamFrames(nil, protocol.MaxByteCount, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f2))
|
||||
Expect(framer.HasData()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("appends to a frame slice", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id1,
|
||||
Data: []byte("foobar"),
|
||||
DataLenPresent: true,
|
||||
}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
f0 := ackhandler.StreamFrame{Frame: &wire.StreamFrame{StreamID: 9999}}
|
||||
frames := []ackhandler.StreamFrame{f0}
|
||||
fs, length := framer.AppendStreamFrames(frames, 1000, protocol.Version1)
|
||||
Expect(fs).To(HaveLen(2))
|
||||
Expect(fs[0]).To(Equal(f0))
|
||||
Expect(fs[1].Frame.Data).To(Equal([]byte("foobar")))
|
||||
Expect(fs[1].Frame.DataLenPresent).To(BeFalse())
|
||||
Expect(length).To(Equal(f.Length(version)))
|
||||
})
|
||||
|
||||
It("skips a stream that was reported active, but was completed shortly after", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(nil, nil)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id2,
|
||||
Data: []byte("foobar"),
|
||||
DataLenPresent: true,
|
||||
}
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AddActiveStream(id2)
|
||||
frames, _ := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f))
|
||||
})
|
||||
|
||||
It("skips a stream that was reported active, but doesn't have any data", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id2,
|
||||
Data: []byte("foobar"),
|
||||
DataLenPresent: true,
|
||||
}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{}, false, false)
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AddActiveStream(id2)
|
||||
frames, _ := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f))
|
||||
})
|
||||
|
||||
It("pops from a stream multiple times, if it has enough data", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil).Times(2)
|
||||
f1 := &wire.StreamFrame{StreamID: id1, Data: []byte("foobar")}
|
||||
f2 := &wire.StreamFrame{StreamID: id1, Data: []byte("foobaz")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f1}, true, true)
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f2}, true, false)
|
||||
framer.AddActiveStream(id1) // only add it once
|
||||
frames, _ := framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f1))
|
||||
frames, _ = framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f2))
|
||||
// no further calls to popStreamFrame, after popStreamFrame said there's no more data
|
||||
frames, _ = framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(BeNil())
|
||||
})
|
||||
|
||||
It("re-queues a stream at the end, if it has enough data", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil).Times(2)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
f11 := &wire.StreamFrame{StreamID: id1, Data: []byte("foobar")}
|
||||
f12 := &wire.StreamFrame{StreamID: id1, Data: []byte("foobaz")}
|
||||
f2 := &wire.StreamFrame{StreamID: id2, Data: []byte("raboof")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f11}, true, true)
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f12}, true, false)
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f2}, true, false)
|
||||
framer.AddActiveStream(id1) // only add it once
|
||||
framer.AddActiveStream(id2)
|
||||
// first a frame from stream 1
|
||||
frames, _ := framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f11))
|
||||
// then a frame from stream 2
|
||||
frames, _ = framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f2))
|
||||
// then another frame from stream 1
|
||||
frames, _ = framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
Expect(frames[0].Frame).To(Equal(f12))
|
||||
})
|
||||
|
||||
It("only dequeues data from each stream once per packet", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
f1 := &wire.StreamFrame{StreamID: id1, Data: []byte("foobar")}
|
||||
f2 := &wire.StreamFrame{StreamID: id2, Data: []byte("raboof")}
|
||||
// both streams have more data, and will be re-queued
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f1}, true, true)
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f2}, true, true)
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AddActiveStream(id2)
|
||||
frames, length := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(2))
|
||||
Expect(frames[0].Frame).To(Equal(f1))
|
||||
Expect(frames[1].Frame).To(Equal(f2))
|
||||
Expect(length).To(Equal(f1.Length(version) + f2.Length(version)))
|
||||
})
|
||||
|
||||
It("returns multiple normal frames in the order they were reported active", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
f1 := &wire.StreamFrame{Data: []byte("foobar")}
|
||||
f2 := &wire.StreamFrame{Data: []byte("foobaz")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f1}, true, false)
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f2}, true, false)
|
||||
framer.AddActiveStream(id2)
|
||||
framer.AddActiveStream(id1)
|
||||
frames, _ := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(2))
|
||||
Expect(frames[0].Frame).To(Equal(f2))
|
||||
Expect(frames[1].Frame).To(Equal(f1))
|
||||
})
|
||||
|
||||
It("only asks a stream for data once, even if it was reported active multiple times", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
f := &wire.StreamFrame{Data: []byte("foobar")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false) // only one call to this function
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AddActiveStream(id1)
|
||||
frames, _ := framer.AppendStreamFrames(nil, 1000, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
})
|
||||
|
||||
It("does not pop empty frames", func() {
|
||||
fs, length := framer.AppendStreamFrames(nil, 500, protocol.Version1)
|
||||
Expect(fs).To(BeEmpty())
|
||||
Expect(length).To(BeZero())
|
||||
})
|
||||
|
||||
It("pops maximum size STREAM frames", func() {
|
||||
for i := protocol.MinStreamFrameSize; i < 2000; i++ {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).DoAndReturn(func(size protocol.ByteCount, v protocol.VersionNumber) (ackhandler.StreamFrame, bool, bool) {
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id1,
|
||||
DataLenPresent: true,
|
||||
}
|
||||
f.Data = make([]byte, f.MaxDataLen(size, v))
|
||||
Expect(f.Length(version)).To(Equal(size))
|
||||
return ackhandler.StreamFrame{Frame: f}, true, false
|
||||
})
|
||||
framer.AddActiveStream(id1)
|
||||
frames, _ := framer.AppendStreamFrames(nil, i, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(1))
|
||||
f := frames[0].Frame
|
||||
Expect(f.DataLenPresent).To(BeFalse())
|
||||
Expect(f.Length(version)).To(Equal(i))
|
||||
}
|
||||
})
|
||||
|
||||
It("pops multiple STREAM frames", func() {
|
||||
for i := 2 * protocol.MinStreamFrameSize; i < 2000; i++ {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id2).Return(stream2, nil)
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).DoAndReturn(func(size protocol.ByteCount, v protocol.VersionNumber) (ackhandler.StreamFrame, bool, bool) {
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id2,
|
||||
DataLenPresent: true,
|
||||
}
|
||||
f.Data = make([]byte, f.MaxDataLen(protocol.MinStreamFrameSize, v))
|
||||
return ackhandler.StreamFrame{Frame: f}, true, false
|
||||
})
|
||||
stream2.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).DoAndReturn(func(size protocol.ByteCount, v protocol.VersionNumber) (ackhandler.StreamFrame, bool, bool) {
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id2,
|
||||
DataLenPresent: true,
|
||||
}
|
||||
f.Data = make([]byte, f.MaxDataLen(size, v))
|
||||
Expect(f.Length(version)).To(Equal(size))
|
||||
return ackhandler.StreamFrame{Frame: f}, true, false
|
||||
})
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AddActiveStream(id2)
|
||||
frames, _ := framer.AppendStreamFrames(nil, i, protocol.Version1)
|
||||
Expect(frames).To(HaveLen(2))
|
||||
f1 := frames[0].Frame
|
||||
f2 := frames[1].Frame
|
||||
Expect(f1.DataLenPresent).To(BeTrue())
|
||||
Expect(f2.DataLenPresent).To(BeFalse())
|
||||
Expect(f1.Length(version) + f2.Length(version)).To(Equal(i))
|
||||
}
|
||||
})
|
||||
|
||||
It("pops frames that when asked for the the minimum STREAM frame size", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
f := &wire.StreamFrame{Data: []byte("foobar")}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize, protocol.Version1)
|
||||
})
|
||||
|
||||
It("does not pop frames smaller than the minimum size", func() {
|
||||
// don't expect a call to PopStreamFrame()
|
||||
framer.AppendStreamFrames(nil, protocol.MinStreamFrameSize-1, protocol.Version1)
|
||||
})
|
||||
|
||||
It("stops iterating when the remaining size is smaller than the minimum STREAM frame size", func() {
|
||||
streamGetter.EXPECT().GetOrOpenSendStream(id1).Return(stream1, nil)
|
||||
// pop a frame such that the remaining size is one byte less than the minimum STREAM frame size
|
||||
f := &wire.StreamFrame{
|
||||
StreamID: id1,
|
||||
Data: bytes.Repeat([]byte("f"), int(500-protocol.MinStreamFrameSize)),
|
||||
DataLenPresent: true,
|
||||
}
|
||||
stream1.EXPECT().popStreamFrame(gomock.Any(), protocol.Version1).Return(ackhandler.StreamFrame{Frame: f}, true, false)
|
||||
framer.AddActiveStream(id1)
|
||||
fs, length := framer.AppendStreamFrames(nil, 500, protocol.Version1)
|
||||
Expect(fs).To(HaveLen(1))
|
||||
Expect(fs[0].Frame).To(Equal(f))
|
||||
Expect(length).To(Equal(f.Length(version)))
|
||||
})
|
||||
|
||||
It("drops all STREAM frames when 0-RTT is rejected", func() {
|
||||
framer.AddActiveStream(id1)
|
||||
Expect(framer.Handle0RTTRejection()).To(Succeed())
|
||||
fs, length := framer.AppendStreamFrames(nil, protocol.MaxByteCount, protocol.Version1)
|
||||
Expect(fs).To(BeEmpty())
|
||||
Expect(length).To(BeZero())
|
||||
})
|
||||
})
|
||||
})
|
287
fuzzing/frames/cmd/corpus.go
Normal file
287
fuzzing/frames/cmd/corpus.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/rand"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
const version = protocol.Version1
|
||||
|
||||
func getRandomData(l int) []byte {
|
||||
b := make([]byte, l)
|
||||
rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func getRandomNumber() uint64 {
|
||||
switch 1 << uint8(rand.Intn(3)) {
|
||||
case 1:
|
||||
return uint64(rand.Int63n(64))
|
||||
case 2:
|
||||
return uint64(rand.Int63n(16384))
|
||||
case 4:
|
||||
return uint64(rand.Int63n(1073741824))
|
||||
case 8:
|
||||
return uint64(rand.Int63n(4611686018427387904))
|
||||
default:
|
||||
panic("unexpected length")
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomNumberLowerOrEqual(target uint64) uint64 {
|
||||
if target == 0 {
|
||||
return 0
|
||||
}
|
||||
return uint64(rand.Int63n(int64(target)))
|
||||
}
|
||||
|
||||
// returns a *maximum* number of num ACK ranges
|
||||
func getAckRanges(num int) []wire.AckRange {
|
||||
var ranges []wire.AckRange
|
||||
|
||||
prevSmallest := uint64(rand.Int63n(4611686018427387904))
|
||||
for i := 0; i < num; i++ {
|
||||
if prevSmallest <= 2 {
|
||||
break
|
||||
}
|
||||
largest := getRandomNumberLowerOrEqual(prevSmallest - 2)
|
||||
smallest := getRandomNumberLowerOrEqual(largest)
|
||||
|
||||
ranges = append(ranges, wire.AckRange{
|
||||
Smallest: protocol.PacketNumber(smallest),
|
||||
Largest: protocol.PacketNumber(largest),
|
||||
})
|
||||
prevSmallest = smallest
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
func getFrames() []wire.Frame {
|
||||
frames := []wire.Frame{
|
||||
&wire.StreamFrame{ // STREAM frame at 0 offset, with FIN bit
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Fin: true,
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame at 0 offset, with data and FIN bit
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Fin: true,
|
||||
Data: getRandomData(100),
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame at non-zero offset, with data
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Offset: protocol.ByteCount(getRandomNumber()),
|
||||
Data: getRandomData(50),
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame at non-zero offset, with data and FIN bit
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Offset: protocol.ByteCount(getRandomNumber()),
|
||||
Data: getRandomData(50),
|
||||
Fin: true,
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame at non-zero offset, with data and FIN bit. Long enough to use the buffer.
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Offset: protocol.ByteCount(getRandomNumber()),
|
||||
Data: getRandomData(2 * protocol.MinStreamFrameBufferSize),
|
||||
Fin: true,
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame at maximum offset, with FIN bit
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Offset: protocol.MaxByteCount - 5,
|
||||
Data: getRandomData(5),
|
||||
Fin: true,
|
||||
},
|
||||
&wire.StreamFrame{ // STREAM frame with data at maximum offset
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
Offset: protocol.MaxByteCount,
|
||||
Data: getRandomData(10),
|
||||
},
|
||||
&wire.AckFrame{
|
||||
AckRanges: getAckRanges(1),
|
||||
DelayTime: time.Duration(getRandomNumber()),
|
||||
},
|
||||
&wire.AckFrame{
|
||||
AckRanges: getAckRanges(5),
|
||||
DelayTime: time.Duration(getRandomNumber()),
|
||||
},
|
||||
&wire.AckFrame{
|
||||
AckRanges: getAckRanges(300),
|
||||
DelayTime: time.Duration(getRandomNumber()),
|
||||
},
|
||||
&wire.AckFrame{
|
||||
AckRanges: getAckRanges(3),
|
||||
DelayTime: time.Duration(getRandomNumber()),
|
||||
ECT0: getRandomNumber(),
|
||||
ECT1: getRandomNumber(),
|
||||
ECNCE: getRandomNumber(),
|
||||
},
|
||||
&wire.PingFrame{},
|
||||
&wire.ResetStreamFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
ErrorCode: quic.StreamErrorCode(getRandomNumber()),
|
||||
FinalSize: protocol.ByteCount(getRandomNumber()),
|
||||
},
|
||||
&wire.ResetStreamFrame{ // at maximum offset
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
ErrorCode: quic.StreamErrorCode(getRandomNumber()),
|
||||
FinalSize: protocol.MaxByteCount,
|
||||
},
|
||||
&wire.StopSendingFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
ErrorCode: quic.StreamErrorCode(getRandomNumber()),
|
||||
},
|
||||
&wire.CryptoFrame{
|
||||
Data: getRandomData(100),
|
||||
},
|
||||
&wire.CryptoFrame{
|
||||
Offset: protocol.ByteCount(getRandomNumber()),
|
||||
Data: getRandomData(50),
|
||||
},
|
||||
&wire.NewTokenFrame{
|
||||
Token: getRandomData(10),
|
||||
},
|
||||
&wire.MaxDataFrame{
|
||||
MaximumData: protocol.ByteCount(getRandomNumber()),
|
||||
},
|
||||
&wire.MaxDataFrame{
|
||||
MaximumData: protocol.MaxByteCount,
|
||||
},
|
||||
&wire.MaxStreamDataFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
MaximumStreamData: protocol.ByteCount(getRandomNumber()),
|
||||
},
|
||||
&wire.MaxStreamDataFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
MaximumStreamData: protocol.MaxByteCount,
|
||||
},
|
||||
&wire.MaxStreamsFrame{
|
||||
Type: protocol.StreamTypeUni,
|
||||
MaxStreamNum: protocol.StreamNum(getRandomNumber()),
|
||||
},
|
||||
&wire.MaxStreamsFrame{
|
||||
Type: protocol.StreamTypeBidi,
|
||||
MaxStreamNum: protocol.StreamNum(getRandomNumber()),
|
||||
},
|
||||
&wire.DataBlockedFrame{
|
||||
MaximumData: protocol.ByteCount(getRandomNumber()),
|
||||
},
|
||||
&wire.DataBlockedFrame{
|
||||
MaximumData: protocol.MaxByteCount,
|
||||
},
|
||||
&wire.StreamDataBlockedFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
MaximumStreamData: protocol.ByteCount(getRandomNumber()),
|
||||
},
|
||||
&wire.StreamDataBlockedFrame{
|
||||
StreamID: protocol.StreamID(getRandomNumber()),
|
||||
MaximumStreamData: protocol.MaxByteCount,
|
||||
},
|
||||
&wire.StreamsBlockedFrame{
|
||||
Type: protocol.StreamTypeUni,
|
||||
StreamLimit: protocol.StreamNum(getRandomNumber()),
|
||||
},
|
||||
&wire.StreamsBlockedFrame{
|
||||
Type: protocol.StreamTypeBidi,
|
||||
StreamLimit: protocol.StreamNum(getRandomNumber()),
|
||||
},
|
||||
&wire.RetireConnectionIDFrame{
|
||||
SequenceNumber: getRandomNumber(),
|
||||
},
|
||||
&wire.ConnectionCloseFrame{ // QUIC error with empty reason
|
||||
IsApplicationError: false,
|
||||
ErrorCode: getRandomNumber(),
|
||||
ReasonPhrase: "",
|
||||
},
|
||||
&wire.ConnectionCloseFrame{ // QUIC error with reason
|
||||
IsApplicationError: false,
|
||||
// TODO: add frame type
|
||||
ErrorCode: getRandomNumber(),
|
||||
ReasonPhrase: string(getRandomData(100)),
|
||||
},
|
||||
&wire.ConnectionCloseFrame{ // application error with empty reason
|
||||
IsApplicationError: true,
|
||||
ErrorCode: getRandomNumber(),
|
||||
ReasonPhrase: "",
|
||||
},
|
||||
&wire.ConnectionCloseFrame{ // application error with reason
|
||||
IsApplicationError: true,
|
||||
ErrorCode: getRandomNumber(),
|
||||
ReasonPhrase: string(getRandomData(100)),
|
||||
},
|
||||
}
|
||||
|
||||
seq1 := getRandomNumber()
|
||||
seq2 := getRandomNumber()
|
||||
var token1, token2 protocol.StatelessResetToken
|
||||
copy(token1[:], getRandomData(16))
|
||||
copy(token2[:], getRandomData(16))
|
||||
frames = append(frames, []wire.Frame{
|
||||
&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: seq1,
|
||||
RetirePriorTo: seq1 / 2,
|
||||
ConnectionID: protocol.ParseConnectionID(getRandomData(4)),
|
||||
StatelessResetToken: token1,
|
||||
},
|
||||
&wire.NewConnectionIDFrame{
|
||||
SequenceNumber: seq2,
|
||||
RetirePriorTo: seq2,
|
||||
ConnectionID: protocol.ParseConnectionID(getRandomData(17)),
|
||||
StatelessResetToken: token2,
|
||||
},
|
||||
}...)
|
||||
|
||||
var data1 [8]byte
|
||||
copy(data1[:], getRandomData(8))
|
||||
frames = append(frames, &wire.PathChallengeFrame{
|
||||
Data: data1,
|
||||
})
|
||||
|
||||
var data2 [8]byte
|
||||
copy(data2[:], getRandomData(8))
|
||||
frames = append(frames, &wire.PathResponseFrame{
|
||||
Data: data2,
|
||||
})
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func main() {
|
||||
for _, f := range getFrames() {
|
||||
b, err := f.Append(nil, version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", b, 1); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 30; i++ {
|
||||
frames := getFrames()
|
||||
|
||||
var b []byte
|
||||
for j := 0; j < rand.Intn(30)+2; j++ {
|
||||
if rand.Intn(10) == 0 { // write a PADDING frame
|
||||
b = append(b, 0)
|
||||
}
|
||||
f := frames[rand.Intn(len(frames))]
|
||||
var err error
|
||||
b, err = f.Append(b, version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if rand.Intn(10) == 0 { // write a PADDING frame
|
||||
b = append(b, 0)
|
||||
}
|
||||
}
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", b, 1); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
82
fuzzing/frames/fuzz.go
Normal file
82
fuzzing/frames/fuzz.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package frames
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
const version = protocol.Version1
|
||||
|
||||
// PrefixLen is the number of bytes used for configuration
|
||||
const PrefixLen = 1
|
||||
|
||||
func toEncLevel(v uint8) protocol.EncryptionLevel {
|
||||
switch v % 3 {
|
||||
default:
|
||||
return protocol.EncryptionInitial
|
||||
case 1:
|
||||
return protocol.EncryptionHandshake
|
||||
case 2:
|
||||
return protocol.Encryption1RTT
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzz fuzzes the QUIC frames.
|
||||
//
|
||||
//go:generate go run ./cmd/corpus.go
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) < PrefixLen {
|
||||
return 0
|
||||
}
|
||||
encLevel := toEncLevel(data[0])
|
||||
data = data[PrefixLen:]
|
||||
|
||||
parser := wire.NewFrameParser(true)
|
||||
parser.SetAckDelayExponent(protocol.DefaultAckDelayExponent)
|
||||
|
||||
var numFrames int
|
||||
var b []byte
|
||||
for len(data) > 0 {
|
||||
initialLen := len(data)
|
||||
l, f, err := parser.ParseNext(data, encLevel, version)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
data = data[l:]
|
||||
numFrames++
|
||||
if f == nil { // PADDING frame
|
||||
continue
|
||||
}
|
||||
// We accept empty STREAM frames, but we don't write them.
|
||||
if sf, ok := f.(*wire.StreamFrame); ok {
|
||||
if sf.DataLen() == 0 {
|
||||
sf.PutBack()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
startLen := len(b)
|
||||
parsedLen := initialLen - len(data)
|
||||
b, err = f.Append(b, version)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error writing frame %#v: %s", f, err))
|
||||
}
|
||||
frameLen := protocol.ByteCount(len(b) - startLen)
|
||||
if f.Length(version) != frameLen {
|
||||
panic(fmt.Sprintf("Inconsistent frame length for %#v: expected %d, got %d", f, frameLen, f.Length(version)))
|
||||
}
|
||||
if sf, ok := f.(*wire.StreamFrame); ok {
|
||||
sf.PutBack()
|
||||
}
|
||||
if frameLen > protocol.ByteCount(parsedLen) {
|
||||
panic(fmt.Sprintf("Serialized length (%d) is longer than parsed length (%d)", len(b), parsedLen))
|
||||
}
|
||||
}
|
||||
|
||||
if numFrames == 0 {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
127
fuzzing/handshake/cmd/corpus.go
Normal file
127
fuzzing/handshake/cmd/corpus.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
fuzzhandshake "github.com/quic-go/quic-go/fuzzing/handshake"
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/internal/handshake"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/testdata"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
const alpn = "fuzz"
|
||||
|
||||
func main() {
|
||||
client := handshake.NewCryptoSetupClient(
|
||||
protocol.ConnectionID{},
|
||||
&wire.TransportParameters{ActiveConnectionIDLimit: 2},
|
||||
&tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
ServerName: "localhost",
|
||||
NextProtos: []string{alpn},
|
||||
RootCAs: testdata.GetRootCA(),
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(1),
|
||||
},
|
||||
false,
|
||||
utils.NewRTTStats(),
|
||||
nil,
|
||||
utils.DefaultLogger.WithPrefix("client"),
|
||||
protocol.Version1,
|
||||
)
|
||||
|
||||
config := testdata.GetTLSConfig()
|
||||
config.NextProtos = []string{alpn}
|
||||
server := handshake.NewCryptoSetupServer(
|
||||
protocol.ConnectionID{},
|
||||
&net.UDPAddr{IP: net.IPv6loopback, Port: 1234},
|
||||
&net.UDPAddr{IP: net.IPv6loopback, Port: 4321},
|
||||
&wire.TransportParameters{ActiveConnectionIDLimit: 2},
|
||||
config,
|
||||
false,
|
||||
utils.NewRTTStats(),
|
||||
nil,
|
||||
utils.DefaultLogger.WithPrefix("server"),
|
||||
protocol.Version1,
|
||||
)
|
||||
|
||||
if err := client.StartHandshake(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := server.StartHandshake(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var clientHandshakeComplete, serverHandshakeComplete bool
|
||||
var messages [][]byte
|
||||
for {
|
||||
clientLoop:
|
||||
for {
|
||||
ev := client.NextEvent()
|
||||
//nolint:exhaustive // only need to process a few events
|
||||
switch ev.Kind {
|
||||
case handshake.EventNoEvent:
|
||||
break clientLoop
|
||||
case handshake.EventWriteInitialData:
|
||||
messages = append(messages, ev.Data)
|
||||
if err := server.HandleMessage(ev.Data, protocol.EncryptionInitial); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case handshake.EventWriteHandshakeData:
|
||||
messages = append(messages, ev.Data)
|
||||
if err := server.HandleMessage(ev.Data, protocol.EncryptionHandshake); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case handshake.EventHandshakeComplete:
|
||||
clientHandshakeComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
serverLoop:
|
||||
for {
|
||||
ev := server.NextEvent()
|
||||
//nolint:exhaustive // only need to process a few events
|
||||
switch ev.Kind {
|
||||
case handshake.EventNoEvent:
|
||||
break serverLoop
|
||||
case handshake.EventWriteInitialData:
|
||||
messages = append(messages, ev.Data)
|
||||
if err := client.HandleMessage(ev.Data, protocol.EncryptionInitial); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case handshake.EventWriteHandshakeData:
|
||||
messages = append(messages, ev.Data)
|
||||
if err := client.HandleMessage(ev.Data, protocol.EncryptionHandshake); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case handshake.EventHandshakeComplete:
|
||||
serverHandshakeComplete = true
|
||||
}
|
||||
}
|
||||
|
||||
if serverHandshakeComplete && clientHandshakeComplete {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ticket, err := server.GetSessionTicket()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if ticket == nil {
|
||||
log.Fatal("expected a session ticket")
|
||||
}
|
||||
messages = append(messages, ticket)
|
||||
|
||||
for _, m := range messages {
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", m, fuzzhandshake.PrefixLen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
421
fuzzing/handshake/fuzz.go
Normal file
421
fuzzing/handshake/fuzz.go
Normal file
|
@ -0,0 +1,421 @@
|
|||
package handshake
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/internal/handshake"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
var (
|
||||
cert, clientCert *tls.Certificate
|
||||
certPool, clientCertPool *x509.CertPool
|
||||
sessionTicketKey = [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
|
||||
)
|
||||
|
||||
func init() {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cert, certPool, err = helper.GenerateCertificate(priv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
privClient, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clientCert, clientCertPool, err = helper.GenerateCertificate(privClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type messageType uint8
|
||||
|
||||
// TLS handshake message types.
|
||||
const (
|
||||
typeClientHello messageType = 1
|
||||
typeServerHello messageType = 2
|
||||
typeNewSessionTicket messageType = 4
|
||||
typeEncryptedExtensions messageType = 8
|
||||
typeCertificate messageType = 11
|
||||
typeCertificateRequest messageType = 13
|
||||
typeCertificateVerify messageType = 15
|
||||
typeFinished messageType = 20
|
||||
)
|
||||
|
||||
func (m messageType) String() string {
|
||||
switch m {
|
||||
case typeClientHello:
|
||||
return "ClientHello"
|
||||
case typeServerHello:
|
||||
return "ServerHello"
|
||||
case typeNewSessionTicket:
|
||||
return "NewSessionTicket"
|
||||
case typeEncryptedExtensions:
|
||||
return "EncryptedExtensions"
|
||||
case typeCertificate:
|
||||
return "Certificate"
|
||||
case typeCertificateRequest:
|
||||
return "CertificateRequest"
|
||||
case typeCertificateVerify:
|
||||
return "CertificateVerify"
|
||||
case typeFinished:
|
||||
return "Finished"
|
||||
default:
|
||||
return fmt.Sprintf("unknown message type: %d", m)
|
||||
}
|
||||
}
|
||||
|
||||
func appendSuites(suites []uint16, rand uint8) []uint16 {
|
||||
const (
|
||||
s1 = tls.TLS_AES_128_GCM_SHA256
|
||||
s2 = tls.TLS_AES_256_GCM_SHA384
|
||||
s3 = tls.TLS_CHACHA20_POLY1305_SHA256
|
||||
)
|
||||
switch rand % 4 {
|
||||
default:
|
||||
return suites
|
||||
case 1:
|
||||
return append(suites, s1)
|
||||
case 2:
|
||||
return append(suites, s2)
|
||||
case 3:
|
||||
return append(suites, s3)
|
||||
}
|
||||
}
|
||||
|
||||
// consumes 2 bits
|
||||
func getSuites(rand uint8) []uint16 {
|
||||
suites := make([]uint16, 0, 3)
|
||||
for i := 1; i <= 3; i++ {
|
||||
suites = appendSuites(suites, rand>>i%4)
|
||||
}
|
||||
return suites
|
||||
}
|
||||
|
||||
// consumes 3 bits
|
||||
func getClientAuth(rand uint8) tls.ClientAuthType {
|
||||
switch rand {
|
||||
default:
|
||||
return tls.NoClientCert
|
||||
case 0:
|
||||
return tls.RequestClientCert
|
||||
case 1:
|
||||
return tls.RequireAnyClientCert
|
||||
case 2:
|
||||
return tls.VerifyClientCertIfGiven
|
||||
case 3:
|
||||
return tls.RequireAndVerifyClientCert
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
alpn = "fuzzing"
|
||||
alpnWrong = "wrong"
|
||||
)
|
||||
|
||||
func toEncryptionLevel(n uint8) protocol.EncryptionLevel {
|
||||
switch n % 3 {
|
||||
default:
|
||||
return protocol.EncryptionInitial
|
||||
case 1:
|
||||
return protocol.EncryptionHandshake
|
||||
case 2:
|
||||
return protocol.Encryption1RTT
|
||||
}
|
||||
}
|
||||
|
||||
func getTransportParameters(seed uint8) *wire.TransportParameters {
|
||||
const maxVarInt = math.MaxUint64 / 4
|
||||
r := mrand.New(mrand.NewSource(int64(seed)))
|
||||
return &wire.TransportParameters{
|
||||
InitialMaxData: protocol.ByteCount(r.Int63n(maxVarInt)),
|
||||
InitialMaxStreamDataBidiLocal: protocol.ByteCount(r.Int63n(maxVarInt)),
|
||||
InitialMaxStreamDataBidiRemote: protocol.ByteCount(r.Int63n(maxVarInt)),
|
||||
InitialMaxStreamDataUni: protocol.ByteCount(r.Int63n(maxVarInt)),
|
||||
}
|
||||
}
|
||||
|
||||
// PrefixLen is the number of bytes used for configuration
|
||||
const (
|
||||
PrefixLen = 12
|
||||
confLen = 5
|
||||
)
|
||||
|
||||
// Fuzz fuzzes the TLS 1.3 handshake used by QUIC.
|
||||
//
|
||||
//go:generate go run ./cmd/corpus.go
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) < PrefixLen {
|
||||
return -1
|
||||
}
|
||||
dataLen := len(data)
|
||||
var runConfig1, runConfig2 [confLen]byte
|
||||
copy(runConfig1[:], data)
|
||||
data = data[confLen:]
|
||||
messageConfig1 := data[0]
|
||||
data = data[1:]
|
||||
copy(runConfig2[:], data)
|
||||
data = data[confLen:]
|
||||
messageConfig2 := data[0]
|
||||
data = data[1:]
|
||||
if dataLen != len(data)+PrefixLen {
|
||||
panic("incorrect configuration")
|
||||
}
|
||||
|
||||
clientConf := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
ServerName: "localhost",
|
||||
NextProtos: []string{alpn},
|
||||
RootCAs: certPool,
|
||||
}
|
||||
useSessionTicketCache := helper.NthBit(runConfig1[0], 2)
|
||||
if useSessionTicketCache {
|
||||
clientConf.ClientSessionCache = tls.NewLRUClientSessionCache(5)
|
||||
}
|
||||
|
||||
if val := runHandshake(runConfig1, messageConfig1, clientConf, data); val != 1 {
|
||||
return val
|
||||
}
|
||||
return runHandshake(runConfig2, messageConfig2, clientConf, data)
|
||||
}
|
||||
|
||||
func runHandshake(runConfig [confLen]byte, messageConfig uint8, clientConf *tls.Config, data []byte) int {
|
||||
serverConf := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
NextProtos: []string{alpn},
|
||||
SessionTicketKey: sessionTicketKey,
|
||||
}
|
||||
|
||||
enable0RTTClient := helper.NthBit(runConfig[0], 0)
|
||||
enable0RTTServer := helper.NthBit(runConfig[0], 1)
|
||||
sendPostHandshakeMessageToClient := helper.NthBit(runConfig[0], 3)
|
||||
sendPostHandshakeMessageToServer := helper.NthBit(runConfig[0], 4)
|
||||
sendSessionTicket := helper.NthBit(runConfig[0], 5)
|
||||
clientConf.CipherSuites = getSuites(runConfig[0] >> 6)
|
||||
serverConf.ClientAuth = getClientAuth(runConfig[1] & 0b00000111)
|
||||
serverConf.CipherSuites = getSuites(runConfig[1] >> 6)
|
||||
serverConf.SessionTicketsDisabled = helper.NthBit(runConfig[1], 3)
|
||||
if helper.NthBit(runConfig[2], 0) {
|
||||
clientConf.RootCAs = x509.NewCertPool()
|
||||
}
|
||||
if helper.NthBit(runConfig[2], 1) {
|
||||
serverConf.ClientCAs = clientCertPool
|
||||
} else {
|
||||
serverConf.ClientCAs = x509.NewCertPool()
|
||||
}
|
||||
if helper.NthBit(runConfig[2], 2) {
|
||||
serverConf.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
if helper.NthBit(runConfig[2], 3) {
|
||||
return nil, errors.New("getting client config failed")
|
||||
}
|
||||
if helper.NthBit(runConfig[2], 4) {
|
||||
return nil, nil
|
||||
}
|
||||
return serverConf, nil
|
||||
}
|
||||
}
|
||||
if helper.NthBit(runConfig[2], 5) {
|
||||
serverConf.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if helper.NthBit(runConfig[2], 6) {
|
||||
return nil, errors.New("getting certificate failed")
|
||||
}
|
||||
if helper.NthBit(runConfig[2], 7) {
|
||||
return nil, nil
|
||||
}
|
||||
return clientCert, nil // this certificate will be invalid
|
||||
}
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 0) {
|
||||
serverConf.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
if helper.NthBit(runConfig[3], 1) {
|
||||
return errors.New("certificate verification failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 2) {
|
||||
clientConf.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
if helper.NthBit(runConfig[3], 3) {
|
||||
return errors.New("certificate verification failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 4) {
|
||||
serverConf.NextProtos = []string{alpnWrong}
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 5) {
|
||||
serverConf.NextProtos = []string{alpnWrong, alpn}
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 6) {
|
||||
serverConf.KeyLogWriter = io.Discard
|
||||
}
|
||||
if helper.NthBit(runConfig[3], 7) {
|
||||
clientConf.KeyLogWriter = io.Discard
|
||||
}
|
||||
clientTP := getTransportParameters(runConfig[4] & 0x3)
|
||||
if helper.NthBit(runConfig[4], 3) {
|
||||
clientTP.MaxAckDelay = protocol.MaxMaxAckDelay + 5
|
||||
}
|
||||
serverTP := getTransportParameters(runConfig[4] & 0b00011000)
|
||||
if helper.NthBit(runConfig[4], 3) {
|
||||
serverTP.MaxAckDelay = protocol.MaxMaxAckDelay + 5
|
||||
}
|
||||
|
||||
messageToReplace := messageConfig % 32
|
||||
messageToReplaceEncLevel := toEncryptionLevel(messageConfig >> 6)
|
||||
|
||||
if len(data) == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
client := handshake.NewCryptoSetupClient(
|
||||
protocol.ConnectionID{},
|
||||
clientTP,
|
||||
clientConf,
|
||||
enable0RTTClient,
|
||||
utils.NewRTTStats(),
|
||||
nil,
|
||||
utils.DefaultLogger.WithPrefix("client"),
|
||||
protocol.Version1,
|
||||
)
|
||||
if err := client.StartHandshake(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
server := handshake.NewCryptoSetupServer(
|
||||
protocol.ConnectionID{},
|
||||
&net.UDPAddr{IP: net.IPv6loopback, Port: 1234},
|
||||
&net.UDPAddr{IP: net.IPv6loopback, Port: 4321},
|
||||
serverTP,
|
||||
serverConf,
|
||||
enable0RTTServer,
|
||||
utils.NewRTTStats(),
|
||||
nil,
|
||||
utils.DefaultLogger.WithPrefix("server"),
|
||||
protocol.Version1,
|
||||
)
|
||||
if err := server.StartHandshake(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var clientHandshakeComplete, serverHandshakeComplete bool
|
||||
for {
|
||||
clientLoop:
|
||||
for {
|
||||
var processedEvent bool
|
||||
ev := client.NextEvent()
|
||||
//nolint:exhaustive // only need to process a few events
|
||||
switch ev.Kind {
|
||||
case handshake.EventNoEvent:
|
||||
if !processedEvent && !clientHandshakeComplete { // handshake stuck
|
||||
return 1
|
||||
}
|
||||
break clientLoop
|
||||
case handshake.EventWriteInitialData, handshake.EventWriteHandshakeData:
|
||||
msg := ev.Data
|
||||
if msg[0] == messageToReplace {
|
||||
fmt.Printf("replacing %s message to the server with %s at %s\n", messageType(msg[0]), messageType(data[0]), messageToReplaceEncLevel)
|
||||
msg = data
|
||||
}
|
||||
if err := server.HandleMessage(msg, messageToReplaceEncLevel); err != nil {
|
||||
return 1
|
||||
}
|
||||
case handshake.EventHandshakeComplete:
|
||||
clientHandshakeComplete = true
|
||||
}
|
||||
processedEvent = true
|
||||
}
|
||||
|
||||
serverLoop:
|
||||
for {
|
||||
var processedEvent bool
|
||||
ev := server.NextEvent()
|
||||
//nolint:exhaustive // only need to process a few events
|
||||
switch ev.Kind {
|
||||
case handshake.EventNoEvent:
|
||||
if !processedEvent && !serverHandshakeComplete { // handshake stuck
|
||||
return 1
|
||||
}
|
||||
break serverLoop
|
||||
case handshake.EventWriteInitialData, handshake.EventWriteHandshakeData:
|
||||
msg := ev.Data
|
||||
if msg[0] == messageToReplace {
|
||||
fmt.Printf("replacing %s message to the client with %s at %s\n", messageType(msg[0]), messageType(data[0]), messageToReplaceEncLevel)
|
||||
msg = data
|
||||
}
|
||||
if err := client.HandleMessage(msg, messageToReplaceEncLevel); err != nil {
|
||||
return 1
|
||||
}
|
||||
case handshake.EventHandshakeComplete:
|
||||
serverHandshakeComplete = true
|
||||
}
|
||||
processedEvent = true
|
||||
}
|
||||
|
||||
if serverHandshakeComplete && clientHandshakeComplete {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = client.ConnectionState()
|
||||
_ = server.ConnectionState()
|
||||
|
||||
sealer, err := client.Get1RTTSealer()
|
||||
if err != nil {
|
||||
panic("expected to get a 1-RTT sealer")
|
||||
}
|
||||
opener, err := server.Get1RTTOpener()
|
||||
if err != nil {
|
||||
panic("expected to get a 1-RTT opener")
|
||||
}
|
||||
const msg = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
encrypted := sealer.Seal(nil, []byte(msg), 1337, []byte("foobar"))
|
||||
decrypted, err := opener.Open(nil, encrypted, time.Time{}, 1337, protocol.KeyPhaseZero, []byte("foobar"))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Decrypting message failed: %s", err.Error()))
|
||||
}
|
||||
if string(decrypted) != msg {
|
||||
panic("wrong message")
|
||||
}
|
||||
|
||||
if sendSessionTicket && !serverConf.SessionTicketsDisabled {
|
||||
ticket, err := server.GetSessionTicket()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ticket == nil {
|
||||
panic("empty ticket")
|
||||
}
|
||||
client.HandleMessage(ticket, protocol.Encryption1RTT)
|
||||
}
|
||||
if sendPostHandshakeMessageToClient {
|
||||
client.HandleMessage(data, messageToReplaceEncLevel)
|
||||
}
|
||||
if sendPostHandshakeMessageToServer {
|
||||
server.HandleMessage(data, messageToReplaceEncLevel)
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
146
fuzzing/header/cmd/corpus.go
Normal file
146
fuzzing/header/cmd/corpus.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/exp/rand"
|
||||
|
||||
"github.com/quic-go/quic-go/fuzzing/header"
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
const version = protocol.Version1
|
||||
|
||||
func getRandomData(l int) []byte {
|
||||
b := make([]byte, l)
|
||||
rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func getVNP(src, dest protocol.ArbitraryLenConnectionID, numVersions int) []byte {
|
||||
versions := make([]protocol.VersionNumber, numVersions)
|
||||
for i := 0; i < numVersions; i++ {
|
||||
versions[i] = protocol.VersionNumber(rand.Uint32())
|
||||
}
|
||||
return wire.ComposeVersionNegotiation(src, dest, versions)
|
||||
}
|
||||
|
||||
func main() {
|
||||
headers := []wire.Header{
|
||||
{ // Initial without token
|
||||
SrcConnectionID: protocol.ParseConnectionID(getRandomData(3)),
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(8)),
|
||||
Type: protocol.PacketTypeInitial,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
},
|
||||
{ // Initial without token, with zero-length src conn id
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(8)),
|
||||
Type: protocol.PacketTypeInitial,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
},
|
||||
{ // Initial with Token
|
||||
SrcConnectionID: protocol.ParseConnectionID(getRandomData(10)),
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(19)),
|
||||
Type: protocol.PacketTypeInitial,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
Token: getRandomData(25),
|
||||
},
|
||||
{ // Handshake packet
|
||||
SrcConnectionID: protocol.ParseConnectionID(getRandomData(5)),
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(10)),
|
||||
Type: protocol.PacketTypeHandshake,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
},
|
||||
{ // Handshake packet, with zero-length src conn id
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(12)),
|
||||
Type: protocol.PacketTypeHandshake,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
},
|
||||
{ // 0-RTT packet
|
||||
SrcConnectionID: protocol.ParseConnectionID(getRandomData(8)),
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(9)),
|
||||
Type: protocol.PacketType0RTT,
|
||||
Length: protocol.ByteCount(rand.Intn(1000)),
|
||||
Version: version,
|
||||
},
|
||||
{ // Retry Packet, with empty orig dest conn id
|
||||
SrcConnectionID: protocol.ParseConnectionID(getRandomData(8)),
|
||||
DestConnectionID: protocol.ParseConnectionID(getRandomData(9)),
|
||||
Type: protocol.PacketTypeRetry,
|
||||
Token: getRandomData(1000),
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
for _, h := range headers {
|
||||
extHdr := &wire.ExtendedHeader{
|
||||
Header: h,
|
||||
PacketNumberLen: protocol.PacketNumberLen(rand.Intn(4) + 1),
|
||||
PacketNumber: protocol.PacketNumber(rand.Uint64()),
|
||||
}
|
||||
b, err := extHdr.Append(nil, version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if h.Type == protocol.PacketTypeRetry {
|
||||
b = append(b, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}...)
|
||||
}
|
||||
if h.Length > 0 {
|
||||
b = append(b, make([]byte, h.Length)...)
|
||||
}
|
||||
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", b, header.PrefixLen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// short header
|
||||
b, err := wire.AppendShortHeader(nil, protocol.ParseConnectionID(getRandomData(8)), 1337, protocol.PacketNumberLen2, protocol.KeyPhaseOne)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", b, header.PrefixLen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
vnps := [][]byte{
|
||||
getVNP(
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(8)),
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(10)),
|
||||
4,
|
||||
),
|
||||
getVNP(
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(10)),
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(5)),
|
||||
0,
|
||||
),
|
||||
getVNP(
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(3)),
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(19)),
|
||||
100,
|
||||
),
|
||||
getVNP(
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(3)),
|
||||
nil,
|
||||
20,
|
||||
),
|
||||
getVNP(
|
||||
nil,
|
||||
protocol.ArbitraryLenConnectionID(getRandomData(10)),
|
||||
5,
|
||||
),
|
||||
}
|
||||
|
||||
for _, vnp := range vnps {
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", vnp, header.PrefixLen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
103
fuzzing/header/fuzz.go
Normal file
103
fuzzing/header/fuzz.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package header
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
const version = protocol.Version1
|
||||
|
||||
// PrefixLen is the number of bytes used for configuration
|
||||
const PrefixLen = 1
|
||||
|
||||
// Fuzz fuzzes the QUIC header.
|
||||
//
|
||||
//go:generate go run ./cmd/corpus.go
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) < PrefixLen {
|
||||
return 0
|
||||
}
|
||||
connIDLen := int(data[0] % 21)
|
||||
data = data[PrefixLen:]
|
||||
|
||||
if wire.IsVersionNegotiationPacket(data) {
|
||||
return fuzzVNP(data)
|
||||
}
|
||||
connID, err := wire.ParseConnectionID(data, connIDLen)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
if !wire.IsLongHeaderPacket(data[0]) {
|
||||
wire.ParseShortHeader(data, connIDLen)
|
||||
return 1
|
||||
}
|
||||
|
||||
is0RTTPacket := wire.Is0RTTPacket(data)
|
||||
hdr, _, _, err := wire.ParsePacket(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if hdr.DestConnectionID != connID {
|
||||
panic(fmt.Sprintf("Expected connection IDs to match: %s vs %s", hdr.DestConnectionID, connID))
|
||||
}
|
||||
if (hdr.Type == protocol.PacketType0RTT) != is0RTTPacket {
|
||||
panic("inconsistent 0-RTT packet detection")
|
||||
}
|
||||
|
||||
var extHdr *wire.ExtendedHeader
|
||||
// Parse the extended header, if this is not a Retry packet.
|
||||
if hdr.Type == protocol.PacketTypeRetry {
|
||||
extHdr = &wire.ExtendedHeader{Header: *hdr}
|
||||
} else {
|
||||
var err error
|
||||
extHdr, err = hdr.ParseExtended(bytes.NewReader(data), version)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
// We always use a 2-byte encoding for the Length field in Long Header packets.
|
||||
// Serializing the header will fail when using a higher value.
|
||||
if hdr.Length > 16383 {
|
||||
return 1
|
||||
}
|
||||
b, err := extHdr.Append(nil, version)
|
||||
if err != nil {
|
||||
// We are able to parse packets with connection IDs longer than 20 bytes,
|
||||
// but in QUIC version 1, we don't write headers with longer connection IDs.
|
||||
if hdr.DestConnectionID.Len() <= protocol.MaxConnIDLen &&
|
||||
hdr.SrcConnectionID.Len() <= protocol.MaxConnIDLen {
|
||||
panic(err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
// GetLength is not implemented for Retry packets
|
||||
if hdr.Type != protocol.PacketTypeRetry {
|
||||
if expLen := extHdr.GetLength(version); expLen != protocol.ByteCount(len(b)) {
|
||||
panic(fmt.Sprintf("inconsistent header length: %#v. Expected %d, got %d", extHdr, expLen, len(b)))
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func fuzzVNP(data []byte) int {
|
||||
connID, err := wire.ParseConnectionID(data, 0)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
dest, src, versions, err := wire.ParseVersionNegotiationPacket(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if !bytes.Equal(dest, connID.Bytes()) {
|
||||
panic("connection IDs don't match")
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
panic("no versions")
|
||||
}
|
||||
wire.ComposeVersionNegotiation(src, dest, versions)
|
||||
return 1
|
||||
}
|
73
fuzzing/internal/helper/helper.go
Normal file
73
fuzzing/internal/helper/helper.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
// NthBit gets the n-th bit of a byte (counting starts at 0).
|
||||
func NthBit(val uint8, n int) bool {
|
||||
if n < 0 || n > 7 {
|
||||
panic("invalid value for n")
|
||||
}
|
||||
return val>>n&0x1 == 1
|
||||
}
|
||||
|
||||
// WriteCorpusFile writes data to a corpus file in directory path.
|
||||
// The filename is calculated from the SHA1 sum of the file contents.
|
||||
func WriteCorpusFile(path string, data []byte) error {
|
||||
// create the directory, if it doesn't exist yet
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hash := sha1.Sum(data)
|
||||
return os.WriteFile(filepath.Join(path, hex.EncodeToString(hash[:])), data, 0o644)
|
||||
}
|
||||
|
||||
// WriteCorpusFileWithPrefix writes data to a corpus file in directory path.
|
||||
// In many fuzzers, the first n bytes are used to control.
|
||||
// This function prepends n zero-bytes to the data.
|
||||
func WriteCorpusFileWithPrefix(path string, data []byte, n int) error {
|
||||
return WriteCorpusFile(path, append(make([]byte, n), data...))
|
||||
}
|
||||
|
||||
// GenerateCertificate generates a self-signed certificate.
|
||||
// It returns the certificate and a x509.CertPool containing that certificate.
|
||||
func GenerateCertificate(priv crypto.Signer) (*tls.Certificate, *x509.CertPool, error) {
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{Organization: []string{"quic-go fuzzer"}},
|
||||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||||
NotAfter: time.Now().Add(30 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"localhost"},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(derBytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(cert)
|
||||
return &tls.Certificate{
|
||||
Certificate: [][]byte{derBytes},
|
||||
PrivateKey: priv,
|
||||
}, certPool, nil
|
||||
}
|
13
fuzzing/internal/helper/helper_suite_test.go
Normal file
13
fuzzing/internal/helper/helper_suite_test.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestHelper(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Helper Suite")
|
||||
}
|
71
fuzzing/internal/helper/helper_test.go
Normal file
71
fuzzing/internal/helper/helper_test.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("exporting", func() {
|
||||
var dir string
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
dir, err = os.MkdirTemp("", "fuzzing-helper")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
fmt.Fprintf(GinkgoWriter, "Created temporary directory %s", dir)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(dir).ToNot(BeEmpty())
|
||||
Expect(os.RemoveAll(dir)).To(Succeed())
|
||||
})
|
||||
|
||||
It("writes a file", func() {
|
||||
const data = "lorem ipsum"
|
||||
// calculated by running sha1sum on the generated file
|
||||
const expectedShaSum = "bfb7759a67daeb65410490b4d98bb9da7d1ea2ce"
|
||||
Expect(WriteCorpusFile(dir, []byte("lorem ipsum"))).To(Succeed())
|
||||
path := filepath.Join(dir, expectedShaSum)
|
||||
Expect(path).To(BeARegularFile())
|
||||
b, err := os.ReadFile(path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(b)).To(Equal(data))
|
||||
})
|
||||
|
||||
It("writes a file and prepends data", func() {
|
||||
const data = "lorem ipsum"
|
||||
// calculated by running sha1sum on the generated file
|
||||
const expectedShaSum = "523f5cab80fab0c7889dbf50dd310ab8c8879f9c"
|
||||
const prefixLen = 7
|
||||
Expect(WriteCorpusFileWithPrefix(dir, []byte("lorem ipsum"), prefixLen)).To(Succeed())
|
||||
path := filepath.Join(dir, expectedShaSum)
|
||||
Expect(path).To(BeARegularFile())
|
||||
b, err := os.ReadFile(path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(b[:prefixLen]).To(Equal(make([]byte, prefixLen)))
|
||||
Expect(string(b[prefixLen:])).To(Equal(data))
|
||||
})
|
||||
|
||||
It("creates the directory, if it doesn't yet", func() {
|
||||
subdir := filepath.Join(dir, "corpus")
|
||||
Expect(subdir).ToNot(BeADirectory())
|
||||
Expect(WriteCorpusFile(subdir, []byte("lorem ipsum"))).To(Succeed())
|
||||
Expect(subdir).To(BeADirectory())
|
||||
})
|
||||
|
||||
It("gets the nth bit of a byte", func() {
|
||||
const val = 0b10010001
|
||||
Expect(NthBit(val, 0)).To(BeTrue())
|
||||
Expect(NthBit(val, 1)).To(BeFalse())
|
||||
Expect(NthBit(val, 2)).To(BeFalse())
|
||||
Expect(NthBit(val, 3)).To(BeFalse())
|
||||
Expect(NthBit(val, 4)).To(BeTrue())
|
||||
Expect(NthBit(val, 5)).To(BeFalse())
|
||||
Expect(NthBit(val, 6)).To(BeFalse())
|
||||
Expect(NthBit(val, 7)).To(BeTrue())
|
||||
})
|
||||
})
|
142
fuzzing/tokens/fuzz.go
Normal file
142
fuzzing/tokens/fuzz.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package tokens
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/handshake"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
)
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) < 8 {
|
||||
return -1
|
||||
}
|
||||
seed := binary.BigEndian.Uint64(data[:8])
|
||||
data = data[8:]
|
||||
tg, err := handshake.NewTokenGenerator(rand.New(rand.NewSource(int64(seed))))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(data) < 1 {
|
||||
return -1
|
||||
}
|
||||
s := data[0] % 3
|
||||
data = data[1:]
|
||||
switch s {
|
||||
case 0:
|
||||
tg.DecodeToken(data)
|
||||
return 1
|
||||
case 1:
|
||||
return newToken(tg, data)
|
||||
case 2:
|
||||
return newRetryToken(tg, data)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func newToken(tg *handshake.TokenGenerator, data []byte) int {
|
||||
if len(data) < 1 {
|
||||
return -1
|
||||
}
|
||||
usesUDPAddr := data[0]%2 == 0
|
||||
data = data[1:]
|
||||
if len(data) != 18 {
|
||||
return -1
|
||||
}
|
||||
var addr net.Addr
|
||||
if usesUDPAddr {
|
||||
addr = &net.UDPAddr{
|
||||
Port: int(binary.BigEndian.Uint16(data[:2])),
|
||||
IP: net.IP(data[2:]),
|
||||
}
|
||||
} else {
|
||||
addr = &net.TCPAddr{
|
||||
Port: int(binary.BigEndian.Uint16(data[:2])),
|
||||
IP: net.IP(data[2:]),
|
||||
}
|
||||
}
|
||||
start := time.Now()
|
||||
encrypted, err := tg.NewToken(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
token, err := tg.DecodeToken(encrypted)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if token.IsRetryToken {
|
||||
panic("didn't encode a Retry token")
|
||||
}
|
||||
if token.SentTime.Before(start) || token.SentTime.After(time.Now()) {
|
||||
panic("incorrect send time")
|
||||
}
|
||||
if token.OriginalDestConnectionID.Len() > 0 || token.RetrySrcConnectionID.Len() > 0 {
|
||||
panic("didn't expect connection IDs")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func newRetryToken(tg *handshake.TokenGenerator, data []byte) int {
|
||||
if len(data) < 2 {
|
||||
return -1
|
||||
}
|
||||
origDestConnIDLen := int(data[0] % 21)
|
||||
retrySrcConnIDLen := int(data[1] % 21)
|
||||
data = data[2:]
|
||||
if len(data) < origDestConnIDLen {
|
||||
return -1
|
||||
}
|
||||
origDestConnID := protocol.ParseConnectionID(data[:origDestConnIDLen])
|
||||
data = data[origDestConnIDLen:]
|
||||
if len(data) < retrySrcConnIDLen {
|
||||
return -1
|
||||
}
|
||||
retrySrcConnID := protocol.ParseConnectionID(data[:retrySrcConnIDLen])
|
||||
data = data[retrySrcConnIDLen:]
|
||||
|
||||
if len(data) < 1 {
|
||||
return -1
|
||||
}
|
||||
usesUDPAddr := data[0]%2 == 0
|
||||
data = data[1:]
|
||||
if len(data) != 18 {
|
||||
return -1
|
||||
}
|
||||
start := time.Now()
|
||||
var addr net.Addr
|
||||
if usesUDPAddr {
|
||||
addr = &net.UDPAddr{
|
||||
Port: int(binary.BigEndian.Uint16(data[:2])),
|
||||
IP: net.IP(data[2:]),
|
||||
}
|
||||
} else {
|
||||
addr = &net.TCPAddr{
|
||||
Port: int(binary.BigEndian.Uint16(data[:2])),
|
||||
IP: net.IP(data[2:]),
|
||||
}
|
||||
}
|
||||
encrypted, err := tg.NewRetryToken(addr, origDestConnID, retrySrcConnID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
token, err := tg.DecodeToken(encrypted)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !token.IsRetryToken {
|
||||
panic("expected a Retry token")
|
||||
}
|
||||
if token.SentTime.Before(start) || token.SentTime.After(time.Now()) {
|
||||
panic("incorrect send time")
|
||||
}
|
||||
if token.OriginalDestConnectionID != origDestConnID {
|
||||
panic("orig dest conn ID doesn't match")
|
||||
}
|
||||
if token.RetrySrcConnectionID != retrySrcConnID {
|
||||
panic("retry src conn ID doesn't match")
|
||||
}
|
||||
return 1
|
||||
}
|
86
fuzzing/transportparameters/cmd/corpus.go
Normal file
86
fuzzing/transportparameters/cmd/corpus.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/rand"
|
||||
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/fuzzing/transportparameters"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
func getRandomData(l int) []byte {
|
||||
b := make([]byte, l)
|
||||
rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func getRandomValue() uint64 {
|
||||
maxVals := []int64{math.MaxUint8 / 4, math.MaxUint16 / 4, math.MaxUint32 / 4, math.MaxUint64 / 4}
|
||||
return uint64(rand.Int63n(maxVals[int(rand.Int31n(4))]))
|
||||
}
|
||||
|
||||
func main() {
|
||||
for i := 0; i < 30; i++ {
|
||||
tp := &wire.TransportParameters{
|
||||
InitialMaxStreamDataBidiLocal: protocol.ByteCount(getRandomValue()),
|
||||
InitialMaxStreamDataBidiRemote: protocol.ByteCount(getRandomValue()),
|
||||
InitialMaxStreamDataUni: protocol.ByteCount(getRandomValue()),
|
||||
InitialMaxData: protocol.ByteCount(getRandomValue()),
|
||||
MaxAckDelay: time.Duration(getRandomValue()),
|
||||
AckDelayExponent: uint8(getRandomValue()),
|
||||
DisableActiveMigration: getRandomValue()%2 == 0,
|
||||
MaxUDPPayloadSize: protocol.ByteCount(getRandomValue()),
|
||||
MaxUniStreamNum: protocol.StreamNum(getRandomValue()),
|
||||
MaxBidiStreamNum: protocol.StreamNum(getRandomValue()),
|
||||
MaxIdleTimeout: time.Duration(getRandomValue()),
|
||||
ActiveConnectionIDLimit: getRandomValue() + 2,
|
||||
}
|
||||
if rand.Int()%2 == 0 {
|
||||
tp.OriginalDestinationConnectionID = protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
|
||||
}
|
||||
if rand.Int()%2 == 0 {
|
||||
tp.InitialSourceConnectionID = protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
|
||||
}
|
||||
if rand.Int()%2 == 0 {
|
||||
connID := protocol.ParseConnectionID(getRandomData(rand.Intn(21)))
|
||||
tp.RetrySourceConnectionID = &connID
|
||||
}
|
||||
if rand.Int()%2 == 0 {
|
||||
var token protocol.StatelessResetToken
|
||||
rand.Read(token[:])
|
||||
tp.StatelessResetToken = &token
|
||||
}
|
||||
if rand.Int()%2 == 0 {
|
||||
var token protocol.StatelessResetToken
|
||||
rand.Read(token[:])
|
||||
tp.PreferredAddress = &wire.PreferredAddress{
|
||||
IPv4: net.IPv4(uint8(rand.Int()), uint8(rand.Int()), uint8(rand.Int()), uint8(rand.Int())),
|
||||
IPv4Port: uint16(rand.Int()),
|
||||
IPv6: net.IP(getRandomData(16)),
|
||||
IPv6Port: uint16(rand.Int()),
|
||||
ConnectionID: protocol.ParseConnectionID(getRandomData(rand.Intn(21))),
|
||||
StatelessResetToken: token,
|
||||
}
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if rand.Int()%2 == 0 {
|
||||
pers := protocol.PerspectiveServer
|
||||
if rand.Int()%2 == 0 {
|
||||
pers = protocol.PerspectiveClient
|
||||
}
|
||||
data = tp.Marshal(pers)
|
||||
} else {
|
||||
data = tp.MarshalForSessionTicket(nil)
|
||||
}
|
||||
if err := helper.WriteCorpusFileWithPrefix("corpus", data, transportparameters.PrefixLen); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
60
fuzzing/transportparameters/fuzz.go
Normal file
60
fuzzing/transportparameters/fuzz.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package transportparameters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go/fuzzing/internal/helper"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/wire"
|
||||
)
|
||||
|
||||
// PrefixLen is the number of bytes used for configuration
|
||||
const PrefixLen = 1
|
||||
|
||||
// Fuzz fuzzes the QUIC transport parameters.
|
||||
//
|
||||
//go:generate go run ./cmd/corpus.go
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) <= PrefixLen {
|
||||
return 0
|
||||
}
|
||||
|
||||
if helper.NthBit(data[0], 0) {
|
||||
return fuzzTransportParametersForSessionTicket(data[PrefixLen:])
|
||||
}
|
||||
return fuzzTransportParameters(data[PrefixLen:], helper.NthBit(data[0], 1))
|
||||
}
|
||||
|
||||
func fuzzTransportParameters(data []byte, isServer bool) int {
|
||||
perspective := protocol.PerspectiveClient
|
||||
if isServer {
|
||||
perspective = protocol.PerspectiveServer
|
||||
}
|
||||
|
||||
tp := &wire.TransportParameters{}
|
||||
if err := tp.Unmarshal(data, perspective); err != nil {
|
||||
return 0
|
||||
}
|
||||
_ = tp.String()
|
||||
|
||||
tp2 := &wire.TransportParameters{}
|
||||
if err := tp2.Unmarshal(tp.Marshal(perspective), perspective); err != nil {
|
||||
fmt.Printf("%#v\n", tp)
|
||||
panic(err)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func fuzzTransportParametersForSessionTicket(data []byte) int {
|
||||
tp := &wire.TransportParameters{}
|
||||
if err := tp.UnmarshalFromSessionTicket(bytes.NewReader(data)); err != nil {
|
||||
return 0
|
||||
}
|
||||
b := tp.MarshalForSessionTicket(nil)
|
||||
tp2 := &wire.TransportParameters{}
|
||||
if err := tp2.UnmarshalFromSessionTicket(bytes.NewReader(b)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return 1
|
||||
}
|
36
go.mod
Normal file
36
go.mod
Normal file
|
@ -0,0 +1,36 @@
|
|||
module github.com/quic-go/quic-go
|
||||
|
||||
go 1.20
|
||||
|
||||
replace github.com/refraction-networking/utls => ../utls
|
||||
|
||||
require (
|
||||
github.com/francoispqt/gojay v1.2.13
|
||||
github.com/gaukas/clienthellod v0.4.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/onsi/ginkgo/v2 v2.9.5
|
||||
github.com/onsi/gomega v1.27.6
|
||||
github.com/quic-go/qpack v0.4.0
|
||||
github.com/refraction-networking/utls v1.3.2
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
|
||||
golang.org/x/net v0.11.0
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sys v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/tools v0.9.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
247
go.sum
Normal file
247
go.sum
Normal file
|
@ -0,0 +1,247 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gaukas/clienthellod v0.4.0 h1:DySeZT4c3Xw6OGMzHRlAuOHx9q1P7vQNjA7YkyHrqac=
|
||||
github.com/gaukas/clienthellod v0.4.0/go.mod h1:gjt7a7cNNzZV4yTe0jKcXtj0a7u6RL2KQvijxFOvcZE=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
135
http3/body.go
Normal file
135
http3/body.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
// The HTTPStreamer allows taking over a HTTP/3 stream. The interface is implemented by:
|
||||
// * for the server: the http.Request.Body
|
||||
// * for the client: the http.Response.Body
|
||||
// On the client side, the stream will be closed for writing, unless the DontCloseRequestStream RoundTripOpt was set.
|
||||
// When a stream is taken over, it's the caller's responsibility to close the stream.
|
||||
type HTTPStreamer interface {
|
||||
HTTPStream() Stream
|
||||
}
|
||||
|
||||
type StreamCreator interface {
|
||||
// Context returns a context that is cancelled when the underlying connection is closed.
|
||||
Context() context.Context
|
||||
OpenStream() (quic.Stream, error)
|
||||
OpenStreamSync(context.Context) (quic.Stream, error)
|
||||
OpenUniStream() (quic.SendStream, error)
|
||||
OpenUniStreamSync(context.Context) (quic.SendStream, error)
|
||||
LocalAddr() net.Addr
|
||||
RemoteAddr() net.Addr
|
||||
ConnectionState() quic.ConnectionState
|
||||
}
|
||||
|
||||
var _ StreamCreator = quic.Connection(nil)
|
||||
|
||||
// A Hijacker allows hijacking of the stream creating part of a quic.Session from a http.Response.Body.
|
||||
// It is used by WebTransport to create WebTransport streams after a session has been established.
|
||||
type Hijacker interface {
|
||||
StreamCreator() StreamCreator
|
||||
}
|
||||
|
||||
// The body of a http.Request or http.Response.
|
||||
type body struct {
|
||||
str quic.Stream
|
||||
|
||||
wasHijacked bool // set when HTTPStream is called
|
||||
}
|
||||
|
||||
var (
|
||||
_ io.ReadCloser = &body{}
|
||||
_ HTTPStreamer = &body{}
|
||||
)
|
||||
|
||||
func newRequestBody(str Stream) *body {
|
||||
return &body{str: str}
|
||||
}
|
||||
|
||||
func (r *body) HTTPStream() Stream {
|
||||
r.wasHijacked = true
|
||||
return r.str
|
||||
}
|
||||
|
||||
func (r *body) wasStreamHijacked() bool {
|
||||
return r.wasHijacked
|
||||
}
|
||||
|
||||
func (r *body) Read(b []byte) (int, error) {
|
||||
return r.str.Read(b)
|
||||
}
|
||||
|
||||
func (r *body) Close() error {
|
||||
r.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
return nil
|
||||
}
|
||||
|
||||
type hijackableBody struct {
|
||||
body
|
||||
conn quic.Connection // only needed to implement Hijacker
|
||||
|
||||
// only set for the http.Response
|
||||
// The channel is closed when the user is done with this response:
|
||||
// either when Read() errors, or when Close() is called.
|
||||
reqDone chan<- struct{}
|
||||
reqDoneClosed bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ Hijacker = &hijackableBody{}
|
||||
_ HTTPStreamer = &hijackableBody{}
|
||||
)
|
||||
|
||||
func newResponseBody(str Stream, conn quic.Connection, done chan<- struct{}) *hijackableBody {
|
||||
return &hijackableBody{
|
||||
body: body{
|
||||
str: str,
|
||||
},
|
||||
reqDone: done,
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *hijackableBody) StreamCreator() StreamCreator {
|
||||
return r.conn
|
||||
}
|
||||
|
||||
func (r *hijackableBody) Read(b []byte) (int, error) {
|
||||
n, err := r.str.Read(b)
|
||||
if err != nil {
|
||||
r.requestDone()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *hijackableBody) requestDone() {
|
||||
if r.reqDoneClosed || r.reqDone == nil {
|
||||
return
|
||||
}
|
||||
if r.reqDone != nil {
|
||||
close(r.reqDone)
|
||||
}
|
||||
r.reqDoneClosed = true
|
||||
}
|
||||
|
||||
func (r *body) StreamID() quic.StreamID {
|
||||
return r.str.StreamID()
|
||||
}
|
||||
|
||||
func (r *hijackableBody) Close() error {
|
||||
r.requestDone()
|
||||
// If the EOF was read, CancelRead() is a no-op.
|
||||
r.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *hijackableBody) HTTPStream() Stream {
|
||||
return r.str
|
||||
}
|
54
http3/body_test.go
Normal file
54
http3/body_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Response Body", func() {
|
||||
var reqDone chan struct{}
|
||||
|
||||
BeforeEach(func() { reqDone = make(chan struct{}) })
|
||||
|
||||
It("closes the reqDone channel when Read errors", func() {
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
str.EXPECT().Read(gomock.Any()).Return(0, errors.New("test error"))
|
||||
rb := newResponseBody(str, nil, reqDone)
|
||||
_, err := rb.Read([]byte{0})
|
||||
Expect(err).To(MatchError("test error"))
|
||||
Expect(reqDone).To(BeClosed())
|
||||
})
|
||||
|
||||
It("allows multiple calls to Read, when Read errors", func() {
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
str.EXPECT().Read(gomock.Any()).Return(0, errors.New("test error")).Times(2)
|
||||
rb := newResponseBody(str, nil, reqDone)
|
||||
_, err := rb.Read([]byte{0})
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(reqDone).To(BeClosed())
|
||||
_, err = rb.Read([]byte{0})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("closes responses", func() {
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
rb := newResponseBody(str, nil, reqDone)
|
||||
str.EXPECT().CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
Expect(rb.Close()).To(Succeed())
|
||||
})
|
||||
|
||||
It("allows multiple calls to Close", func() {
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
rb := newResponseBody(str, nil, reqDone)
|
||||
str.EXPECT().CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled)).MaxTimes(2)
|
||||
Expect(rb.Close()).To(Succeed())
|
||||
Expect(reqDone).To(BeClosed())
|
||||
Expect(rb.Close()).To(Succeed())
|
||||
})
|
||||
})
|
55
http3/capsule.go
Normal file
55
http3/capsule.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
// CapsuleType is the type of the capsule.
|
||||
type CapsuleType uint64
|
||||
|
||||
type exactReader struct {
|
||||
R *io.LimitedReader
|
||||
}
|
||||
|
||||
func (r *exactReader) Read(b []byte) (int, error) {
|
||||
n, err := r.R.Read(b)
|
||||
if r.R.N > 0 {
|
||||
return n, io.ErrUnexpectedEOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ParseCapsule parses the header of a Capsule.
|
||||
// It returns an io.LimitedReader that can be used to read the Capsule value.
|
||||
// The Capsule value must be read entirely (i.e. until the io.EOF) before using r again.
|
||||
func ParseCapsule(r quicvarint.Reader) (CapsuleType, io.Reader, error) {
|
||||
ct, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return 0, nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
l, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return 0, nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
return CapsuleType(ct), &exactReader{R: io.LimitReader(r, int64(l)).(*io.LimitedReader)}, nil
|
||||
}
|
||||
|
||||
// WriteCapsule writes a capsule
|
||||
func WriteCapsule(w quicvarint.Writer, ct CapsuleType, value []byte) error {
|
||||
b := make([]byte, 0, 16)
|
||||
b = quicvarint.Append(b, uint64(ct))
|
||||
b = quicvarint.Append(b, uint64(len(value)))
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(value)
|
||||
return err
|
||||
}
|
55
http3/capsule_test.go
Normal file
55
http3/capsule_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Capsule", func() {
|
||||
It("parses Capsules", func() {
|
||||
b := quicvarint.Append(nil, 1337)
|
||||
b = quicvarint.Append(b, 6)
|
||||
b = append(b, []byte("foobar")...)
|
||||
|
||||
ct, r, err := ParseCapsule(bytes.NewReader(b))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ct).To(BeEquivalentTo(1337))
|
||||
val, err := io.ReadAll(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(val)).To(Equal("foobar"))
|
||||
})
|
||||
|
||||
It("writes capsules", func() {
|
||||
var buf bytes.Buffer
|
||||
WriteCapsule(&buf, 1337, []byte("foobar"))
|
||||
|
||||
ct, r, err := ParseCapsule(&buf)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ct).To(BeEquivalentTo(1337))
|
||||
val, err := io.ReadAll(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(val)).To(Equal("foobar"))
|
||||
})
|
||||
|
||||
It("errors on EOF", func() {
|
||||
b := quicvarint.Append(nil, 1337)
|
||||
b = quicvarint.Append(b, 6)
|
||||
b = append(b, []byte("foobar")...)
|
||||
|
||||
for i := range b {
|
||||
ct, r, err := ParseCapsule(bytes.NewReader(b[:i]))
|
||||
if err != nil {
|
||||
Expect(err).To(MatchError(io.ErrUnexpectedEOF))
|
||||
continue
|
||||
}
|
||||
Expect(ct).To(BeEquivalentTo(1337))
|
||||
_, err = io.ReadAll(r)
|
||||
Expect(err).To(Equal(io.ErrUnexpectedEOF))
|
||||
}
|
||||
})
|
||||
})
|
498
http3/client.go
Normal file
498
http3/client.go
Normal file
|
@ -0,0 +1,498 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
ctls "crypto/tls"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
|
||||
"github.com/quic-go/qpack"
|
||||
)
|
||||
|
||||
// MethodGet0RTT allows a GET request to be sent using 0-RTT.
|
||||
// Note that 0-RTT data doesn't provide replay protection.
|
||||
const MethodGet0RTT = "GET_0RTT"
|
||||
|
||||
const (
|
||||
defaultUserAgent = "quic-go HTTP/3"
|
||||
defaultMaxResponseHeaderBytes = 10 * 1 << 20 // 10 MB
|
||||
)
|
||||
|
||||
var defaultQuicConfig = &quic.Config{
|
||||
MaxIncomingStreams: -1, // don't allow the server to create bidirectional streams
|
||||
KeepAlivePeriod: 10 * time.Second,
|
||||
}
|
||||
|
||||
type dialFunc func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error)
|
||||
|
||||
var dialAddr dialFunc = quic.DialAddrEarly
|
||||
|
||||
type roundTripperOpts struct {
|
||||
DisableCompression bool
|
||||
EnableDatagram bool
|
||||
MaxHeaderBytes int64
|
||||
AdditionalSettings map[uint64]uint64
|
||||
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
|
||||
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
|
||||
}
|
||||
|
||||
// client is a HTTP3 client doing requests
|
||||
type client struct {
|
||||
tlsConf *tls.Config
|
||||
config *quic.Config
|
||||
opts *roundTripperOpts
|
||||
|
||||
dialOnce sync.Once
|
||||
dialer dialFunc
|
||||
handshakeErr error
|
||||
|
||||
requestWriter *requestWriter
|
||||
|
||||
decoder *qpack.Decoder
|
||||
|
||||
hostname string
|
||||
conn atomic.Pointer[quic.EarlyConnection]
|
||||
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
var _ roundTripCloser = &client{}
|
||||
|
||||
func newClient(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) {
|
||||
if conf == nil {
|
||||
conf = defaultQuicConfig.Clone()
|
||||
}
|
||||
if len(conf.Versions) == 0 {
|
||||
conf = conf.Clone()
|
||||
conf.Versions = []quic.VersionNumber{protocol.SupportedVersions[0]}
|
||||
}
|
||||
if len(conf.Versions) != 1 {
|
||||
return nil, errors.New("can only use a single QUIC version for dialing a HTTP/3 connection")
|
||||
}
|
||||
if conf.MaxIncomingStreams == 0 {
|
||||
conf.MaxIncomingStreams = -1 // don't allow any bidirectional streams
|
||||
}
|
||||
conf.EnableDatagrams = opts.EnableDatagram
|
||||
logger := utils.DefaultLogger.WithPrefix("h3 client")
|
||||
|
||||
if tlsConf == nil {
|
||||
tlsConf = &tls.Config{}
|
||||
} else {
|
||||
tlsConf = tlsConf.Clone()
|
||||
}
|
||||
if tlsConf.ServerName == "" {
|
||||
sni, _, err := net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
|
||||
sni = hostname
|
||||
}
|
||||
tlsConf.ServerName = sni
|
||||
}
|
||||
// Replace existing ALPNs by H3
|
||||
tlsConf.NextProtos = []string{versionToALPN(conf.Versions[0])}
|
||||
|
||||
return &client{
|
||||
hostname: authorityAddr("https", hostname),
|
||||
tlsConf: tlsConf,
|
||||
requestWriter: newRequestWriter(logger),
|
||||
decoder: qpack.NewDecoder(func(hf qpack.HeaderField) {}),
|
||||
config: conf,
|
||||
opts: opts,
|
||||
dialer: dialer,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) dial(ctx context.Context) error {
|
||||
var err error
|
||||
var conn quic.EarlyConnection
|
||||
if c.dialer != nil {
|
||||
conn, err = c.dialer(ctx, c.hostname, c.tlsConf, c.config)
|
||||
} else {
|
||||
conn, err = dialAddr(ctx, c.hostname, c.tlsConf, c.config)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn.Store(&conn)
|
||||
|
||||
// send the SETTINGs frame, using 0-RTT data, if possible
|
||||
go func() {
|
||||
if err := c.setupConn(conn); err != nil {
|
||||
c.logger.Debugf("Setting up connection failed: %s", err)
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
|
||||
}
|
||||
}()
|
||||
|
||||
if c.opts.StreamHijacker != nil {
|
||||
go c.handleBidirectionalStreams(conn)
|
||||
}
|
||||
go c.handleUnidirectionalStreams(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) setupConn(conn quic.EarlyConnection) error {
|
||||
// open the control stream
|
||||
str, err := conn.OpenUniStream()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := make([]byte, 0, 64)
|
||||
b = quicvarint.Append(b, streamTypeControlStream)
|
||||
// send the SETTINGS frame
|
||||
b = (&settingsFrame{Datagram: c.opts.EnableDatagram, Other: c.opts.AdditionalSettings}).Append(b)
|
||||
_, err = str.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) handleBidirectionalStreams(conn quic.EarlyConnection) {
|
||||
for {
|
||||
str, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
c.logger.Debugf("accepting bidirectional stream failed: %s", err)
|
||||
return
|
||||
}
|
||||
go func(str quic.Stream) {
|
||||
_, err := parseNextFrame(str, func(ft FrameType, e error) (processed bool, err error) {
|
||||
return c.opts.StreamHijacker(ft, conn, str, e)
|
||||
})
|
||||
if err == errHijacked {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.logger.Debugf("error handling stream: %s", err)
|
||||
}
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
|
||||
}(str)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) handleUnidirectionalStreams(conn quic.EarlyConnection) {
|
||||
for {
|
||||
str, err := conn.AcceptUniStream(context.Background())
|
||||
if err != nil {
|
||||
c.logger.Debugf("accepting unidirectional stream failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func(str quic.ReceiveStream) {
|
||||
streamType, err := quicvarint.Read(quicvarint.NewReader(str))
|
||||
if err != nil {
|
||||
if c.opts.UniStreamHijacker != nil && c.opts.UniStreamHijacker(StreamType(streamType), conn, str, err) {
|
||||
return
|
||||
}
|
||||
c.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err)
|
||||
return
|
||||
}
|
||||
// We're only interested in the control stream here.
|
||||
switch streamType {
|
||||
case streamTypeControlStream:
|
||||
case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream:
|
||||
// Our QPACK implementation doesn't use the dynamic table yet.
|
||||
// TODO: check that only one stream of each type is opened.
|
||||
return
|
||||
case streamTypePushStream:
|
||||
// We never increased the Push ID, so we don't expect any push streams.
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
|
||||
return
|
||||
default:
|
||||
if c.opts.UniStreamHijacker != nil && c.opts.UniStreamHijacker(StreamType(streamType), conn, str, nil) {
|
||||
return
|
||||
}
|
||||
str.CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError))
|
||||
return
|
||||
}
|
||||
f, err := parseNextFrame(str, nil)
|
||||
if err != nil {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
|
||||
return
|
||||
}
|
||||
sf, ok := f.(*settingsFrame)
|
||||
if !ok {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
|
||||
return
|
||||
}
|
||||
if !sf.Datagram {
|
||||
return
|
||||
}
|
||||
// If datagram support was enabled on our side as well as on the server side,
|
||||
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
|
||||
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
|
||||
if c.opts.EnableDatagram && !conn.ConnectionState().SupportsDatagrams {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
|
||||
}
|
||||
}(str)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Close() error {
|
||||
conn := c.conn.Load()
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
return (*conn).CloseWithError(quic.ApplicationErrorCode(ErrCodeNoError), "")
|
||||
}
|
||||
|
||||
func (c *client) maxHeaderBytes() uint64 {
|
||||
if c.opts.MaxHeaderBytes <= 0 {
|
||||
return defaultMaxResponseHeaderBytes
|
||||
}
|
||||
return uint64(c.opts.MaxHeaderBytes)
|
||||
}
|
||||
|
||||
// RoundTripOpt executes a request and returns a response
|
||||
func (c *client) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
if authorityAddr("https", hostnameFromRequest(req)) != c.hostname {
|
||||
return nil, fmt.Errorf("http3 client BUG: RoundTripOpt called for the wrong client (expected %s, got %s)", c.hostname, req.Host)
|
||||
}
|
||||
|
||||
c.dialOnce.Do(func() {
|
||||
c.handshakeErr = c.dial(req.Context())
|
||||
})
|
||||
if c.handshakeErr != nil {
|
||||
return nil, c.handshakeErr
|
||||
}
|
||||
|
||||
// At this point, c.conn is guaranteed to be set.
|
||||
conn := *c.conn.Load()
|
||||
|
||||
// Immediately send out this request, if this is a 0-RTT request.
|
||||
if req.Method == MethodGet0RTT {
|
||||
req.Method = http.MethodGet
|
||||
} else {
|
||||
// wait for the handshake to complete
|
||||
select {
|
||||
case <-conn.HandshakeComplete():
|
||||
case <-req.Context().Done():
|
||||
return nil, req.Context().Err()
|
||||
}
|
||||
}
|
||||
|
||||
str, err := conn.OpenStreamSync(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Request Cancellation:
|
||||
// This go routine keeps running even after RoundTripOpt() returns.
|
||||
// It is shut down when the application is done processing the body.
|
||||
reqDone := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
case <-reqDone:
|
||||
}
|
||||
}()
|
||||
|
||||
doneChan := reqDone
|
||||
if opt.DontCloseRequestStream {
|
||||
doneChan = nil
|
||||
}
|
||||
rsp, rerr := c.doRequest(req, conn, str, opt, doneChan)
|
||||
if rerr.err != nil { // if any error occurred
|
||||
close(reqDone)
|
||||
<-done
|
||||
if rerr.streamErr != 0 { // if it was a stream error
|
||||
str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
|
||||
}
|
||||
if rerr.connErr != 0 { // if it was a connection error
|
||||
var reason string
|
||||
if rerr.err != nil {
|
||||
reason = rerr.err.Error()
|
||||
}
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
|
||||
}
|
||||
return nil, rerr.err
|
||||
}
|
||||
if opt.DontCloseRequestStream {
|
||||
close(reqDone)
|
||||
<-done
|
||||
}
|
||||
return rsp, rerr.err
|
||||
}
|
||||
|
||||
// cancelingReader reads from the io.Reader.
|
||||
// It cancels writing on the stream if any error other than io.EOF occurs.
|
||||
type cancelingReader struct {
|
||||
r io.Reader
|
||||
str Stream
|
||||
}
|
||||
|
||||
func (r *cancelingReader) Read(b []byte) (int, error) {
|
||||
n, err := r.r.Read(b)
|
||||
if err != nil && err != io.EOF {
|
||||
r.str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *client) sendRequestBody(str Stream, body io.ReadCloser, contentLength int64) error {
|
||||
defer body.Close()
|
||||
buf := make([]byte, bodyCopyBufferSize)
|
||||
sr := &cancelingReader{str: str, r: body}
|
||||
if contentLength == -1 {
|
||||
_, err := io.CopyBuffer(str, sr, buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure we don't send more bytes than the content length
|
||||
n, err := io.CopyBuffer(str, io.LimitReader(sr, contentLength), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extra int64
|
||||
extra, err = io.CopyBuffer(io.Discard, sr, buf)
|
||||
n += extra
|
||||
if n > contentLength {
|
||||
str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
|
||||
return fmt.Errorf("http: ContentLength=%d with Body length %d", contentLength, n)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) doRequest(req *http.Request, conn quic.EarlyConnection, str quic.Stream, opt RoundTripOpt, reqDone chan<- struct{}) (*http.Response, requestError) {
|
||||
var requestGzip bool
|
||||
if !c.opts.DisableCompression && req.Method != "HEAD" && req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Range") == "" {
|
||||
requestGzip = true
|
||||
}
|
||||
if err := c.requestWriter.WriteRequestHeader(str, req, requestGzip); err != nil {
|
||||
return nil, newStreamError(ErrCodeInternalError, err)
|
||||
}
|
||||
|
||||
if req.Body == nil && !opt.DontCloseRequestStream {
|
||||
str.Close()
|
||||
}
|
||||
|
||||
hstr := newStream(str, func() { conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "") })
|
||||
if req.Body != nil {
|
||||
// send the request body asynchronously
|
||||
go func() {
|
||||
contentLength := int64(-1)
|
||||
// According to the documentation for http.Request.ContentLength,
|
||||
// a value of 0 with a non-nil Body is also treated as unknown content length.
|
||||
if req.ContentLength > 0 {
|
||||
contentLength = req.ContentLength
|
||||
}
|
||||
if err := c.sendRequestBody(hstr, req.Body, contentLength); err != nil {
|
||||
c.logger.Errorf("Error writing request: %s", err)
|
||||
}
|
||||
if !opt.DontCloseRequestStream {
|
||||
hstr.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
frame, err := parseNextFrame(str, nil)
|
||||
if err != nil {
|
||||
return nil, newStreamError(ErrCodeFrameError, err)
|
||||
}
|
||||
hf, ok := frame.(*headersFrame)
|
||||
if !ok {
|
||||
return nil, newConnError(ErrCodeFrameUnexpected, errors.New("expected first frame to be a HEADERS frame"))
|
||||
}
|
||||
if hf.Length > c.maxHeaderBytes() {
|
||||
return nil, newStreamError(ErrCodeFrameError, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, c.maxHeaderBytes()))
|
||||
}
|
||||
headerBlock := make([]byte, hf.Length)
|
||||
if _, err := io.ReadFull(str, headerBlock); err != nil {
|
||||
return nil, newStreamError(ErrCodeRequestIncomplete, err)
|
||||
}
|
||||
hfs, err := c.decoder.DecodeFull(headerBlock)
|
||||
if err != nil {
|
||||
// TODO: use the right error code
|
||||
return nil, newConnError(ErrCodeGeneralProtocolError, err)
|
||||
}
|
||||
|
||||
res, err := responseFromHeaders(hfs)
|
||||
if err != nil {
|
||||
return nil, newStreamError(ErrCodeMessageError, err)
|
||||
}
|
||||
connState := conn.ConnectionState().TLS
|
||||
|
||||
// [UQUIC] copy utls.ConnectionState to crypto/tls.ConnectionState
|
||||
cryptoConnState := &ctls.ConnectionState{
|
||||
Version: connState.Version,
|
||||
HandshakeComplete: connState.HandshakeComplete,
|
||||
DidResume: connState.DidResume,
|
||||
CipherSuite: connState.CipherSuite,
|
||||
NegotiatedProtocol: connState.NegotiatedProtocol,
|
||||
NegotiatedProtocolIsMutual: connState.NegotiatedProtocolIsMutual,
|
||||
ServerName: connState.ServerName,
|
||||
PeerCertificates: connState.PeerCertificates,
|
||||
VerifiedChains: connState.VerifiedChains,
|
||||
SignedCertificateTimestamps: connState.SignedCertificateTimestamps,
|
||||
OCSPResponse: connState.OCSPResponse,
|
||||
TLSUnique: connState.TLSUnique,
|
||||
}
|
||||
res.TLS = cryptoConnState
|
||||
// [/UQUIC]
|
||||
|
||||
res.Request = req
|
||||
// Check that the server doesn't send more data in DATA frames than indicated by the Content-Length header (if set).
|
||||
// See section 4.1.2 of RFC 9114.
|
||||
var httpStr Stream
|
||||
if _, ok := res.Header["Content-Length"]; ok && res.ContentLength >= 0 {
|
||||
httpStr = newLengthLimitedStream(hstr, res.ContentLength)
|
||||
} else {
|
||||
httpStr = hstr
|
||||
}
|
||||
respBody := newResponseBody(httpStr, conn, reqDone)
|
||||
|
||||
// Rules for when to set Content-Length are defined in https://tools.ietf.org/html/rfc7230#section-3.3.2.
|
||||
_, hasTransferEncoding := res.Header["Transfer-Encoding"]
|
||||
isInformational := res.StatusCode >= 100 && res.StatusCode < 200
|
||||
isNoContent := res.StatusCode == http.StatusNoContent
|
||||
isSuccessfulConnect := req.Method == http.MethodConnect && res.StatusCode >= 200 && res.StatusCode < 300
|
||||
if !hasTransferEncoding && !isInformational && !isNoContent && !isSuccessfulConnect {
|
||||
res.ContentLength = -1
|
||||
if clens, ok := res.Header["Content-Length"]; ok && len(clens) == 1 {
|
||||
if clen64, err := strconv.ParseInt(clens[0], 10, 64); err == nil {
|
||||
res.ContentLength = clen64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if requestGzip && res.Header.Get("Content-Encoding") == "gzip" {
|
||||
res.Header.Del("Content-Encoding")
|
||||
res.Header.Del("Content-Length")
|
||||
res.ContentLength = -1
|
||||
res.Body = newGzipReader(respBody)
|
||||
res.Uncompressed = true
|
||||
} else {
|
||||
res.Body = respBody
|
||||
}
|
||||
|
||||
return res, requestError{}
|
||||
}
|
||||
|
||||
func (c *client) HandshakeComplete() bool {
|
||||
conn := c.conn.Load()
|
||||
if conn == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-(*conn).HandshakeComplete():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
1043
http3/client_test.go
Normal file
1043
http3/client_test.go
Normal file
File diff suppressed because it is too large
Load diff
73
http3/error_codes.go
Normal file
73
http3/error_codes.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type ErrCode quic.ApplicationErrorCode
|
||||
|
||||
const (
|
||||
ErrCodeNoError ErrCode = 0x100
|
||||
ErrCodeGeneralProtocolError ErrCode = 0x101
|
||||
ErrCodeInternalError ErrCode = 0x102
|
||||
ErrCodeStreamCreationError ErrCode = 0x103
|
||||
ErrCodeClosedCriticalStream ErrCode = 0x104
|
||||
ErrCodeFrameUnexpected ErrCode = 0x105
|
||||
ErrCodeFrameError ErrCode = 0x106
|
||||
ErrCodeExcessiveLoad ErrCode = 0x107
|
||||
ErrCodeIDError ErrCode = 0x108
|
||||
ErrCodeSettingsError ErrCode = 0x109
|
||||
ErrCodeMissingSettings ErrCode = 0x10a
|
||||
ErrCodeRequestRejected ErrCode = 0x10b
|
||||
ErrCodeRequestCanceled ErrCode = 0x10c
|
||||
ErrCodeRequestIncomplete ErrCode = 0x10d
|
||||
ErrCodeMessageError ErrCode = 0x10e
|
||||
ErrCodeConnectError ErrCode = 0x10f
|
||||
ErrCodeVersionFallback ErrCode = 0x110
|
||||
ErrCodeDatagramError ErrCode = 0x4a1268
|
||||
)
|
||||
|
||||
func (e ErrCode) String() string {
|
||||
switch e {
|
||||
case ErrCodeNoError:
|
||||
return "H3_NO_ERROR"
|
||||
case ErrCodeGeneralProtocolError:
|
||||
return "H3_GENERAL_PROTOCOL_ERROR"
|
||||
case ErrCodeInternalError:
|
||||
return "H3_INTERNAL_ERROR"
|
||||
case ErrCodeStreamCreationError:
|
||||
return "H3_STREAM_CREATION_ERROR"
|
||||
case ErrCodeClosedCriticalStream:
|
||||
return "H3_CLOSED_CRITICAL_STREAM"
|
||||
case ErrCodeFrameUnexpected:
|
||||
return "H3_FRAME_UNEXPECTED"
|
||||
case ErrCodeFrameError:
|
||||
return "H3_FRAME_ERROR"
|
||||
case ErrCodeExcessiveLoad:
|
||||
return "H3_EXCESSIVE_LOAD"
|
||||
case ErrCodeIDError:
|
||||
return "H3_ID_ERROR"
|
||||
case ErrCodeSettingsError:
|
||||
return "H3_SETTINGS_ERROR"
|
||||
case ErrCodeMissingSettings:
|
||||
return "H3_MISSING_SETTINGS"
|
||||
case ErrCodeRequestRejected:
|
||||
return "H3_REQUEST_REJECTED"
|
||||
case ErrCodeRequestCanceled:
|
||||
return "H3_REQUEST_CANCELLED"
|
||||
case ErrCodeRequestIncomplete:
|
||||
return "H3_INCOMPLETE_REQUEST"
|
||||
case ErrCodeMessageError:
|
||||
return "H3_MESSAGE_ERROR"
|
||||
case ErrCodeConnectError:
|
||||
return "H3_CONNECT_ERROR"
|
||||
case ErrCodeVersionFallback:
|
||||
return "H3_VERSION_FALLBACK"
|
||||
case ErrCodeDatagramError:
|
||||
return "H3_DATAGRAM_ERROR"
|
||||
default:
|
||||
return fmt.Sprintf("unknown error code: %#x", uint16(e))
|
||||
}
|
||||
}
|
39
http3/error_codes_test.go
Normal file
39
http3/error_codes_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("error codes", func() {
|
||||
It("has a string representation for every error code", func() {
|
||||
// We parse the error code file, extract all constants, and verify that
|
||||
// each of them has a string version. Go FTW!
|
||||
_, thisfile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("Failed to get current frame")
|
||||
}
|
||||
filename := path.Join(path.Dir(thisfile), "error_codes.go")
|
||||
fileAst, err := parser.ParseFile(token.NewFileSet(), filename, nil, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
constSpecs := fileAst.Decls[2].(*ast.GenDecl).Specs
|
||||
Expect(len(constSpecs)).To(BeNumerically(">", 4)) // at time of writing
|
||||
for _, c := range constSpecs {
|
||||
valString := c.(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value
|
||||
val, err := strconv.ParseInt(valString, 0, 64)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ErrCode(val).String()).ToNot(Equal("unknown error code"))
|
||||
}
|
||||
})
|
||||
|
||||
It("has a string representation for unknown error codes", func() {
|
||||
Expect(ErrCode(0x1337).String()).To(Equal("unknown error code: 0x1337"))
|
||||
})
|
||||
})
|
164
http3/frames.go
Normal file
164
http3/frames.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
// FrameType is the frame type of a HTTP/3 frame
|
||||
type FrameType uint64
|
||||
|
||||
type unknownFrameHandlerFunc func(FrameType, error) (processed bool, err error)
|
||||
|
||||
type frame interface{}
|
||||
|
||||
var errHijacked = errors.New("hijacked")
|
||||
|
||||
func parseNextFrame(r io.Reader, unknownFrameHandler unknownFrameHandlerFunc) (frame, error) {
|
||||
qr := quicvarint.NewReader(r)
|
||||
for {
|
||||
t, err := quicvarint.Read(qr)
|
||||
if err != nil {
|
||||
if unknownFrameHandler != nil {
|
||||
hijacked, err := unknownFrameHandler(0, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hijacked {
|
||||
return nil, errHijacked
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Call the unknownFrameHandler for frames not defined in the HTTP/3 spec
|
||||
if t > 0xd && unknownFrameHandler != nil {
|
||||
hijacked, err := unknownFrameHandler(FrameType(t), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hijacked {
|
||||
return nil, errHijacked
|
||||
}
|
||||
// If the unknownFrameHandler didn't process the frame, it is our responsibility to skip it.
|
||||
}
|
||||
l, err := quicvarint.Read(qr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch t {
|
||||
case 0x0:
|
||||
return &dataFrame{Length: l}, nil
|
||||
case 0x1:
|
||||
return &headersFrame{Length: l}, nil
|
||||
case 0x4:
|
||||
return parseSettingsFrame(r, l)
|
||||
case 0x3: // CANCEL_PUSH
|
||||
case 0x5: // PUSH_PROMISE
|
||||
case 0x7: // GOAWAY
|
||||
case 0xd: // MAX_PUSH_ID
|
||||
}
|
||||
// skip over unknown frames
|
||||
if _, err := io.CopyN(io.Discard, qr, int64(l)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type dataFrame struct {
|
||||
Length uint64
|
||||
}
|
||||
|
||||
func (f *dataFrame) Append(b []byte) []byte {
|
||||
b = quicvarint.Append(b, 0x0)
|
||||
return quicvarint.Append(b, f.Length)
|
||||
}
|
||||
|
||||
type headersFrame struct {
|
||||
Length uint64
|
||||
}
|
||||
|
||||
func (f *headersFrame) Append(b []byte) []byte {
|
||||
b = quicvarint.Append(b, 0x1)
|
||||
return quicvarint.Append(b, f.Length)
|
||||
}
|
||||
|
||||
const settingDatagram = 0xffd277
|
||||
|
||||
type settingsFrame struct {
|
||||
Datagram bool
|
||||
Other map[uint64]uint64 // all settings that we don't explicitly recognize
|
||||
}
|
||||
|
||||
func parseSettingsFrame(r io.Reader, l uint64) (*settingsFrame, error) {
|
||||
if l > 8*(1<<10) {
|
||||
return nil, fmt.Errorf("unexpected size for SETTINGS frame: %d", l)
|
||||
}
|
||||
buf := make([]byte, l)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
frame := &settingsFrame{}
|
||||
b := bytes.NewReader(buf)
|
||||
var readDatagram bool
|
||||
for b.Len() > 0 {
|
||||
id, err := quicvarint.Read(b)
|
||||
if err != nil { // should not happen. We allocated the whole frame already.
|
||||
return nil, err
|
||||
}
|
||||
val, err := quicvarint.Read(b)
|
||||
if err != nil { // should not happen. We allocated the whole frame already.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch id {
|
||||
case settingDatagram:
|
||||
if readDatagram {
|
||||
return nil, fmt.Errorf("duplicate setting: %d", id)
|
||||
}
|
||||
readDatagram = true
|
||||
if val != 0 && val != 1 {
|
||||
return nil, fmt.Errorf("invalid value for H3_DATAGRAM: %d", val)
|
||||
}
|
||||
frame.Datagram = val == 1
|
||||
default:
|
||||
if _, ok := frame.Other[id]; ok {
|
||||
return nil, fmt.Errorf("duplicate setting: %d", id)
|
||||
}
|
||||
if frame.Other == nil {
|
||||
frame.Other = make(map[uint64]uint64)
|
||||
}
|
||||
frame.Other[id] = val
|
||||
}
|
||||
}
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (f *settingsFrame) Append(b []byte) []byte {
|
||||
b = quicvarint.Append(b, 0x4)
|
||||
var l protocol.ByteCount
|
||||
for id, val := range f.Other {
|
||||
l += quicvarint.Len(id) + quicvarint.Len(val)
|
||||
}
|
||||
if f.Datagram {
|
||||
l += quicvarint.Len(settingDatagram) + quicvarint.Len(1)
|
||||
}
|
||||
b = quicvarint.Append(b, uint64(l))
|
||||
if f.Datagram {
|
||||
b = quicvarint.Append(b, settingDatagram)
|
||||
b = quicvarint.Append(b, 1)
|
||||
}
|
||||
for id, val := range f.Other {
|
||||
b = quicvarint.Append(b, id)
|
||||
b = quicvarint.Append(b, val)
|
||||
}
|
||||
return b
|
||||
}
|
229
http3/frames_test.go
Normal file
229
http3/frames_test.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type errReader struct{ err error }
|
||||
|
||||
func (e errReader) Read([]byte) (int, error) { return 0, e.err }
|
||||
|
||||
var _ = Describe("Frames", func() {
|
||||
It("skips unknown frames", func() {
|
||||
b := quicvarint.Append(nil, 0xdeadbeef) // type byte
|
||||
b = quicvarint.Append(b, 0x42)
|
||||
b = append(b, make([]byte, 0x42)...)
|
||||
b = (&dataFrame{Length: 0x1234}).Append(b)
|
||||
r := bytes.NewReader(b)
|
||||
frame, err := parseNextFrame(r, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&dataFrame{}))
|
||||
Expect(frame.(*dataFrame).Length).To(Equal(uint64(0x1234)))
|
||||
})
|
||||
|
||||
Context("DATA frames", func() {
|
||||
It("parses", func() {
|
||||
data := quicvarint.Append(nil, 0) // type byte
|
||||
data = quicvarint.Append(data, 0x1337)
|
||||
frame, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&dataFrame{}))
|
||||
Expect(frame.(*dataFrame).Length).To(Equal(uint64(0x1337)))
|
||||
})
|
||||
|
||||
It("writes", func() {
|
||||
b := (&dataFrame{Length: 0xdeadbeef}).Append(nil)
|
||||
frame, err := parseNextFrame(bytes.NewReader(b), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&dataFrame{}))
|
||||
Expect(frame.(*dataFrame).Length).To(Equal(uint64(0xdeadbeef)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("HEADERS frames", func() {
|
||||
It("parses", func() {
|
||||
data := quicvarint.Append(nil, 1) // type byte
|
||||
data = quicvarint.Append(data, 0x1337)
|
||||
frame, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&headersFrame{}))
|
||||
Expect(frame.(*headersFrame).Length).To(Equal(uint64(0x1337)))
|
||||
})
|
||||
|
||||
It("writes", func() {
|
||||
b := (&headersFrame{Length: 0xdeadbeef}).Append(nil)
|
||||
frame, err := parseNextFrame(bytes.NewReader(b), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&headersFrame{}))
|
||||
Expect(frame.(*headersFrame).Length).To(Equal(uint64(0xdeadbeef)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("SETTINGS frames", func() {
|
||||
It("parses", func() {
|
||||
settings := quicvarint.Append(nil, 13)
|
||||
settings = quicvarint.Append(settings, 37)
|
||||
settings = quicvarint.Append(settings, 0xdead)
|
||||
settings = quicvarint.Append(settings, 0xbeef)
|
||||
data := quicvarint.Append(nil, 4) // type byte
|
||||
data = quicvarint.Append(data, uint64(len(settings)))
|
||||
data = append(data, settings...)
|
||||
frame, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&settingsFrame{}))
|
||||
sf := frame.(*settingsFrame)
|
||||
Expect(sf.Other).To(HaveKeyWithValue(uint64(13), uint64(37)))
|
||||
Expect(sf.Other).To(HaveKeyWithValue(uint64(0xdead), uint64(0xbeef)))
|
||||
})
|
||||
|
||||
It("rejects duplicate settings", func() {
|
||||
settings := quicvarint.Append(nil, 13)
|
||||
settings = quicvarint.Append(settings, 37)
|
||||
settings = quicvarint.Append(settings, 13)
|
||||
settings = quicvarint.Append(settings, 38)
|
||||
data := quicvarint.Append(nil, 4) // type byte
|
||||
data = quicvarint.Append(data, uint64(len(settings)))
|
||||
data = append(data, settings...)
|
||||
_, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).To(MatchError("duplicate setting: 13"))
|
||||
})
|
||||
|
||||
It("writes", func() {
|
||||
sf := &settingsFrame{Other: map[uint64]uint64{
|
||||
1: 2,
|
||||
99: 999,
|
||||
13: 37,
|
||||
}}
|
||||
frame, err := parseNextFrame(bytes.NewReader(sf.Append(nil)), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(Equal(sf))
|
||||
})
|
||||
|
||||
It("errors on EOF", func() {
|
||||
sf := &settingsFrame{Other: map[uint64]uint64{
|
||||
13: 37,
|
||||
0xdeadbeef: 0xdecafbad,
|
||||
}}
|
||||
data := sf.Append(nil)
|
||||
|
||||
_, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for i := range data {
|
||||
b := make([]byte, i)
|
||||
copy(b, data[:i])
|
||||
_, err := parseNextFrame(bytes.NewReader(b), nil)
|
||||
Expect(err).To(MatchError(io.EOF))
|
||||
}
|
||||
})
|
||||
|
||||
Context("H3_DATAGRAM", func() {
|
||||
It("reads the H3_DATAGRAM value", func() {
|
||||
settings := quicvarint.Append(nil, settingDatagram)
|
||||
settings = quicvarint.Append(settings, 1)
|
||||
data := quicvarint.Append(nil, 4) // type byte
|
||||
data = quicvarint.Append(data, uint64(len(settings)))
|
||||
data = append(data, settings...)
|
||||
f, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(f).To(BeAssignableToTypeOf(&settingsFrame{}))
|
||||
sf := f.(*settingsFrame)
|
||||
Expect(sf.Datagram).To(BeTrue())
|
||||
})
|
||||
|
||||
It("rejects duplicate H3_DATAGRAM entries", func() {
|
||||
settings := quicvarint.Append(nil, settingDatagram)
|
||||
settings = quicvarint.Append(settings, 1)
|
||||
settings = quicvarint.Append(settings, settingDatagram)
|
||||
settings = quicvarint.Append(settings, 1)
|
||||
data := quicvarint.Append(nil, 4) // type byte
|
||||
data = quicvarint.Append(data, uint64(len(settings)))
|
||||
data = append(data, settings...)
|
||||
_, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).To(MatchError(fmt.Sprintf("duplicate setting: %d", settingDatagram)))
|
||||
})
|
||||
|
||||
It("rejects invalid values for the H3_DATAGRAM entry", func() {
|
||||
settings := quicvarint.Append(nil, settingDatagram)
|
||||
settings = quicvarint.Append(settings, 1337)
|
||||
data := quicvarint.Append(nil, 4) // type byte
|
||||
data = quicvarint.Append(data, uint64(len(settings)))
|
||||
data = append(data, settings...)
|
||||
_, err := parseNextFrame(bytes.NewReader(data), nil)
|
||||
Expect(err).To(MatchError("invalid value for H3_DATAGRAM: 1337"))
|
||||
})
|
||||
|
||||
It("writes the H3_DATAGRAM setting", func() {
|
||||
sf := &settingsFrame{Datagram: true}
|
||||
frame, err := parseNextFrame(bytes.NewReader(sf.Append(nil)), nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(Equal(sf))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("hijacking", func() {
|
||||
It("reads a frame without hijacking the stream", func() {
|
||||
buf := bytes.NewBuffer(quicvarint.Append(nil, 1337))
|
||||
customFrameContents := []byte("foobar")
|
||||
buf.Write(customFrameContents)
|
||||
|
||||
var called bool
|
||||
_, err := parseNextFrame(buf, func(ft FrameType, e error) (hijacked bool, err error) {
|
||||
Expect(e).ToNot(HaveOccurred())
|
||||
Expect(ft).To(BeEquivalentTo(1337))
|
||||
called = true
|
||||
b := make([]byte, 3)
|
||||
_, err = io.ReadFull(buf, b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(b)).To(Equal("foo"))
|
||||
return true, nil
|
||||
})
|
||||
Expect(err).To(MatchError(errHijacked))
|
||||
Expect(called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("passes on errors that occur when reading the frame type", func() {
|
||||
testErr := errors.New("test error")
|
||||
var called bool
|
||||
_, err := parseNextFrame(errReader{err: testErr}, func(ft FrameType, e error) (hijacked bool, err error) {
|
||||
Expect(e).To(MatchError(testErr))
|
||||
Expect(ft).To(BeZero())
|
||||
called = true
|
||||
return true, nil
|
||||
})
|
||||
Expect(err).To(MatchError(errHijacked))
|
||||
Expect(called).To(BeTrue())
|
||||
})
|
||||
|
||||
It("reads a frame without hijacking the stream", func() {
|
||||
b := quicvarint.Append(nil, 1337)
|
||||
customFrameContents := []byte("custom frame")
|
||||
b = quicvarint.Append(b, uint64(len(customFrameContents)))
|
||||
b = append(b, customFrameContents...)
|
||||
b = (&dataFrame{Length: 6}).Append(b)
|
||||
b = append(b, []byte("foobar")...)
|
||||
|
||||
var called bool
|
||||
frame, err := parseNextFrame(bytes.NewReader(b), func(ft FrameType, e error) (hijacked bool, err error) {
|
||||
Expect(e).ToNot(HaveOccurred())
|
||||
Expect(ft).To(BeEquivalentTo(1337))
|
||||
called = true
|
||||
return false, nil
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(Equal(&dataFrame{Length: 6}))
|
||||
Expect(called).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
39
http3/gzip_reader.go
Normal file
39
http3/gzip_reader.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package http3
|
||||
|
||||
// copied from net/transport.go
|
||||
|
||||
// gzipReader wraps a response body so it can lazily
|
||||
// call gzip.NewReader on the first call to Read
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
)
|
||||
|
||||
// call gzip.NewReader on the first call to Read
|
||||
type gzipReader struct {
|
||||
body io.ReadCloser // underlying Response.Body
|
||||
zr *gzip.Reader // lazily-initialized gzip reader
|
||||
zerr error // sticky error
|
||||
}
|
||||
|
||||
func newGzipReader(body io.ReadCloser) io.ReadCloser {
|
||||
return &gzipReader{body: body}
|
||||
}
|
||||
|
||||
func (gz *gzipReader) Read(p []byte) (n int, err error) {
|
||||
if gz.zerr != nil {
|
||||
return 0, gz.zerr
|
||||
}
|
||||
if gz.zr == nil {
|
||||
gz.zr, err = gzip.NewReader(gz.body)
|
||||
if err != nil {
|
||||
gz.zerr = err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return gz.zr.Read(p)
|
||||
}
|
||||
|
||||
func (gz *gzipReader) Close() error {
|
||||
return gz.body.Close()
|
||||
}
|
198
http3/headers.go
Normal file
198
http3/headers.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
|
||||
"github.com/quic-go/qpack"
|
||||
)
|
||||
|
||||
type header struct {
|
||||
// Pseudo header fields defined in RFC 9114
|
||||
Path string
|
||||
Method string
|
||||
Authority string
|
||||
Scheme string
|
||||
Status string
|
||||
// for Extended connect
|
||||
Protocol string
|
||||
// parsed and deduplicated
|
||||
ContentLength int64
|
||||
// all non-pseudo headers
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) {
|
||||
hdr := header{Headers: make(http.Header, len(headers))}
|
||||
var readFirstRegularHeader, readContentLength bool
|
||||
var contentLengthStr string
|
||||
for _, h := range headers {
|
||||
// field names need to be lowercase, see section 4.2 of RFC 9114
|
||||
if strings.ToLower(h.Name) != h.Name {
|
||||
return header{}, fmt.Errorf("header field is not lower-case: %s", h.Name)
|
||||
}
|
||||
if !httpguts.ValidHeaderFieldValue(h.Value) {
|
||||
return header{}, fmt.Errorf("invalid header field value for %s: %q", h.Name, h.Value)
|
||||
}
|
||||
if h.IsPseudo() {
|
||||
if readFirstRegularHeader {
|
||||
// all pseudo headers must appear before regular header fields, see section 4.3 of RFC 9114
|
||||
return header{}, fmt.Errorf("received pseudo header %s after a regular header field", h.Name)
|
||||
}
|
||||
var isResponsePseudoHeader bool // pseudo headers are either valid for requests or for responses
|
||||
switch h.Name {
|
||||
case ":path":
|
||||
hdr.Path = h.Value
|
||||
case ":method":
|
||||
hdr.Method = h.Value
|
||||
case ":authority":
|
||||
hdr.Authority = h.Value
|
||||
case ":protocol":
|
||||
hdr.Protocol = h.Value
|
||||
case ":scheme":
|
||||
hdr.Scheme = h.Value
|
||||
case ":status":
|
||||
hdr.Status = h.Value
|
||||
isResponsePseudoHeader = true
|
||||
default:
|
||||
return header{}, fmt.Errorf("unknown pseudo header: %s", h.Name)
|
||||
}
|
||||
if isRequest && isResponsePseudoHeader {
|
||||
return header{}, fmt.Errorf("invalid request pseudo header: %s", h.Name)
|
||||
}
|
||||
if !isRequest && !isResponsePseudoHeader {
|
||||
return header{}, fmt.Errorf("invalid response pseudo header: %s", h.Name)
|
||||
}
|
||||
} else {
|
||||
if !httpguts.ValidHeaderFieldName(h.Name) {
|
||||
return header{}, fmt.Errorf("invalid header field name: %q", h.Name)
|
||||
}
|
||||
readFirstRegularHeader = true
|
||||
switch h.Name {
|
||||
case "content-length":
|
||||
// Ignore duplicate Content-Length headers.
|
||||
// Fail if the duplicates differ.
|
||||
if !readContentLength {
|
||||
readContentLength = true
|
||||
contentLengthStr = h.Value
|
||||
} else if contentLengthStr != h.Value {
|
||||
return header{}, fmt.Errorf("contradicting content lengths (%s and %s)", contentLengthStr, h.Value)
|
||||
}
|
||||
default:
|
||||
hdr.Headers.Add(h.Name, h.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(contentLengthStr) > 0 {
|
||||
// use ParseUint instead of ParseInt, so that parsing fails on negative values
|
||||
cl, err := strconv.ParseUint(contentLengthStr, 10, 63)
|
||||
if err != nil {
|
||||
return header{}, fmt.Errorf("invalid content length: %w", err)
|
||||
}
|
||||
hdr.Headers.Set("Content-Length", contentLengthStr)
|
||||
hdr.ContentLength = int64(cl)
|
||||
}
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error) {
|
||||
hdr, err := parseHeaders(headerFields, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// concatenate cookie headers, see https://tools.ietf.org/html/rfc6265#section-5.4
|
||||
if len(hdr.Headers["Cookie"]) > 0 {
|
||||
hdr.Headers.Set("Cookie", strings.Join(hdr.Headers["Cookie"], "; "))
|
||||
}
|
||||
|
||||
isConnect := hdr.Method == http.MethodConnect
|
||||
// Extended CONNECT, see https://datatracker.ietf.org/doc/html/rfc8441#section-4
|
||||
isExtendedConnected := isConnect && hdr.Protocol != ""
|
||||
if isExtendedConnected {
|
||||
if hdr.Scheme == "" || hdr.Path == "" || hdr.Authority == "" {
|
||||
return nil, errors.New("extended CONNECT: :scheme, :path and :authority must not be empty")
|
||||
}
|
||||
} else if isConnect {
|
||||
if hdr.Path != "" || hdr.Authority == "" { // normal CONNECT
|
||||
return nil, errors.New(":path must be empty and :authority must not be empty")
|
||||
}
|
||||
} else if len(hdr.Path) == 0 || len(hdr.Authority) == 0 || len(hdr.Method) == 0 {
|
||||
return nil, errors.New(":path, :authority and :method must not be empty")
|
||||
}
|
||||
|
||||
var u *url.URL
|
||||
var requestURI string
|
||||
var protocol string
|
||||
|
||||
if isConnect {
|
||||
u = &url.URL{}
|
||||
if isExtendedConnected {
|
||||
u, err = url.ParseRequestURI(hdr.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
u.Path = hdr.Path
|
||||
}
|
||||
u.Scheme = hdr.Scheme
|
||||
u.Host = hdr.Authority
|
||||
requestURI = hdr.Authority
|
||||
protocol = hdr.Protocol
|
||||
} else {
|
||||
protocol = "HTTP/3.0"
|
||||
u, err = url.ParseRequestURI(hdr.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid content length: %w", err)
|
||||
}
|
||||
requestURI = hdr.Path
|
||||
}
|
||||
|
||||
return &http.Request{
|
||||
Method: hdr.Method,
|
||||
URL: u,
|
||||
Proto: protocol,
|
||||
ProtoMajor: 3,
|
||||
ProtoMinor: 0,
|
||||
Header: hdr.Headers,
|
||||
Body: nil,
|
||||
ContentLength: hdr.ContentLength,
|
||||
Host: hdr.Authority,
|
||||
RequestURI: requestURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func hostnameFromRequest(req *http.Request) string {
|
||||
if req.URL != nil {
|
||||
return req.URL.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func responseFromHeaders(headerFields []qpack.HeaderField) (*http.Response, error) {
|
||||
hdr, err := parseHeaders(headerFields, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hdr.Status == "" {
|
||||
return nil, errors.New("missing status field")
|
||||
}
|
||||
rsp := &http.Response{
|
||||
Proto: "HTTP/3.0",
|
||||
ProtoMajor: 3,
|
||||
Header: hdr.Headers,
|
||||
ContentLength: hdr.ContentLength,
|
||||
}
|
||||
status, err := strconv.Atoi(hdr.Status)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid status code: %w", err)
|
||||
}
|
||||
rsp.StatusCode = status
|
||||
rsp.Status = hdr.Status + " " + http.StatusText(status)
|
||||
return rsp, nil
|
||||
}
|
357
http3/headers_test.go
Normal file
357
http3/headers_test.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/quic-go/qpack"
|
||||
)
|
||||
|
||||
var _ = Describe("Request", func() {
|
||||
It("populates requests", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Method).To(Equal("GET"))
|
||||
Expect(req.URL.Path).To(Equal("/foo"))
|
||||
Expect(req.URL.Host).To(BeEmpty())
|
||||
Expect(req.Proto).To(Equal("HTTP/3.0"))
|
||||
Expect(req.ProtoMajor).To(Equal(3))
|
||||
Expect(req.ProtoMinor).To(BeZero())
|
||||
Expect(req.ContentLength).To(Equal(int64(42)))
|
||||
Expect(req.Header).To(HaveLen(1))
|
||||
Expect(req.Header.Get("Content-Length")).To(Equal("42"))
|
||||
Expect(req.Body).To(BeNil())
|
||||
Expect(req.Host).To(Equal("quic.clemente.io"))
|
||||
Expect(req.RequestURI).To(Equal("/foo"))
|
||||
})
|
||||
|
||||
It("rejects upper-case fields", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "Content-Length", Value: "42"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("header field is not lower-case: Content-Length"))
|
||||
})
|
||||
|
||||
It("rejects unknown pseudo headers", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":foo", Value: "bar"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("unknown pseudo header: :foo"))
|
||||
})
|
||||
|
||||
It("rejects invalid field names", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "@", Value: "42"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(`invalid header field name: "@"`))
|
||||
})
|
||||
|
||||
It("rejects invalid field values", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "content", Value: "\n"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(`invalid header field value for content: "\n"`))
|
||||
})
|
||||
|
||||
It("rejects pseudo header fields after regular header fields", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("received pseudo header :authority after a regular header field"))
|
||||
})
|
||||
|
||||
It("rejects negative Content-Length values", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "content-length", Value: "-42"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("invalid content length"))
|
||||
})
|
||||
|
||||
It("rejects multiple Content-Length headers, if they differ", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
{Name: "content-length", Value: "1337"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("contradicting content lengths (42 and 1337)"))
|
||||
})
|
||||
|
||||
It("deduplicates multiple Content-Length headers, if they're the same", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(req.ContentLength).To(Equal(int64(42)))
|
||||
Expect(req.Header.Get("Content-Length")).To(Equal("42"))
|
||||
})
|
||||
|
||||
It("rejects pseudo header fields defined for responses", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":status", Value: "404"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("invalid request pseudo header: :status"))
|
||||
})
|
||||
|
||||
It("parses path with leading double slashes", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "//foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Header).To(BeEmpty())
|
||||
Expect(req.Body).To(BeNil())
|
||||
Expect(req.URL.Path).To(Equal("//foo"))
|
||||
Expect(req.URL.Host).To(BeEmpty())
|
||||
Expect(req.Host).To(Equal("quic.clemente.io"))
|
||||
Expect(req.RequestURI).To(Equal("//foo"))
|
||||
})
|
||||
|
||||
It("concatenates the cookie headers", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "cookie", Value: "cookie1=foobar1"},
|
||||
{Name: "cookie", Value: "cookie2=foobar2"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Header).To(Equal(http.Header{
|
||||
"Cookie": []string{"cookie1=foobar1; cookie2=foobar2"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("handles Other headers", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: "cache-control", Value: "max-age=0"},
|
||||
{Name: "duplicate-header", Value: "1"},
|
||||
{Name: "duplicate-header", Value: "2"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Header).To(Equal(http.Header{
|
||||
"Cache-Control": []string{"max-age=0"},
|
||||
"Duplicate-Header": []string{"1", "2"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("errors with missing path", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
|
||||
})
|
||||
|
||||
It("errors with missing method", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
|
||||
})
|
||||
|
||||
It("errors with missing authority", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(":path, :authority and :method must not be empty"))
|
||||
})
|
||||
|
||||
Context("regular HTTP CONNECT", func() {
|
||||
It("handles CONNECT method", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: http.MethodConnect},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Method).To(Equal(http.MethodConnect))
|
||||
Expect(req.RequestURI).To(Equal("quic.clemente.io"))
|
||||
})
|
||||
|
||||
It("errors with missing authority in CONNECT method", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":method", Value: http.MethodConnect},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
|
||||
})
|
||||
|
||||
It("errors with extra path in CONNECT method", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":path", Value: "/foo"},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":method", Value: http.MethodConnect},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError(":path must be empty and :authority must not be empty"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Extended CONNECT", func() {
|
||||
It("handles Extended CONNECT method", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":protocol", Value: "webtransport"},
|
||||
{Name: ":scheme", Value: "ftp"},
|
||||
{Name: ":method", Value: http.MethodConnect},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":path", Value: "/foo?val=1337"},
|
||||
}
|
||||
req, err := requestFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(req.Method).To(Equal(http.MethodConnect))
|
||||
Expect(req.Proto).To(Equal("webtransport"))
|
||||
Expect(req.URL.String()).To(Equal("ftp://quic.clemente.io/foo?val=1337"))
|
||||
Expect(req.URL.Query().Get("val")).To(Equal("1337"))
|
||||
})
|
||||
|
||||
It("errors with missing scheme", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":protocol", Value: "webtransport"},
|
||||
{Name: ":method", Value: http.MethodConnect},
|
||||
{Name: ":authority", Value: "quic.clemente.io"},
|
||||
{Name: ":path", Value: "/foo"},
|
||||
}
|
||||
_, err := requestFromHeaders(headers)
|
||||
Expect(err).To(MatchError("extended CONNECT: :scheme, :path and :authority must not be empty"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("extracting the hostname from a request", func() {
|
||||
var url *url.URL
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
url, err = url.Parse("https://quic.clemente.io:1337")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("uses req.URL.Host", func() {
|
||||
req := &http.Request{URL: url}
|
||||
Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337"))
|
||||
})
|
||||
|
||||
It("uses req.URL.Host even if req.Host is available", func() {
|
||||
req := &http.Request{
|
||||
Host: "www.example.org",
|
||||
URL: url,
|
||||
}
|
||||
Expect(hostnameFromRequest(req)).To(Equal("quic.clemente.io:1337"))
|
||||
})
|
||||
|
||||
It("returns an empty hostname if nothing is set", func() {
|
||||
Expect(hostnameFromRequest(&http.Request{})).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Response", func() {
|
||||
It("populates responses", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
}
|
||||
rsp, err := responseFromHeaders(headers)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(rsp.Proto).To(Equal("HTTP/3.0"))
|
||||
Expect(rsp.ProtoMajor).To(Equal(3))
|
||||
Expect(rsp.ProtoMinor).To(BeZero())
|
||||
Expect(rsp.ContentLength).To(Equal(int64(42)))
|
||||
Expect(rsp.Header).To(HaveLen(1))
|
||||
Expect(rsp.Header.Get("Content-Length")).To(Equal("42"))
|
||||
Expect(rsp.Body).To(BeNil())
|
||||
Expect(rsp.StatusCode).To(BeEquivalentTo(200))
|
||||
Expect(rsp.Status).To(Equal("200 OK"))
|
||||
})
|
||||
|
||||
It("rejects pseudo header fields after regular header fields", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: "content-length", Value: "42"},
|
||||
{Name: ":status", Value: "200"},
|
||||
}
|
||||
_, err := responseFromHeaders(headers)
|
||||
Expect(err).To(MatchError("received pseudo header :status after a regular header field"))
|
||||
})
|
||||
|
||||
It("rejects response with no status field", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: "content-length", Value: "42"},
|
||||
}
|
||||
_, err := responseFromHeaders(headers)
|
||||
Expect(err).To(MatchError("missing status field"))
|
||||
})
|
||||
|
||||
It("rejects invalid status codes", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":status", Value: "foobar"},
|
||||
{Name: "content-length", Value: "42"},
|
||||
}
|
||||
_, err := responseFromHeaders(headers)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("invalid status code"))
|
||||
})
|
||||
|
||||
It("rejects pseudo header fields defined for requests", func() {
|
||||
headers := []qpack.HeaderField{
|
||||
{Name: ":status", Value: "404"},
|
||||
{Name: ":method", Value: "GET"},
|
||||
}
|
||||
_, err := responseFromHeaders(headers)
|
||||
Expect(err).To(MatchError("invalid response pseudo header: :method"))
|
||||
})
|
||||
})
|
38
http3/http3_suite_test.go
Normal file
38
http3/http3_suite_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestHttp3(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HTTP/3 Suite")
|
||||
}
|
||||
|
||||
var mockCtrl *gomock.Controller
|
||||
|
||||
var _ = BeforeEach(func() {
|
||||
mockCtrl = gomock.NewController(GinkgoT())
|
||||
})
|
||||
|
||||
var _ = AfterEach(func() {
|
||||
mockCtrl.Finish()
|
||||
})
|
||||
|
||||
//nolint:unparam
|
||||
func scaleDuration(t time.Duration) time.Duration {
|
||||
scaleFactor := 1
|
||||
if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
|
||||
scaleFactor = f
|
||||
}
|
||||
Expect(scaleFactor).ToNot(BeZero())
|
||||
return time.Duration(scaleFactor) * t
|
||||
}
|
124
http3/http_stream.go
Normal file
124
http3/http_stream.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
)
|
||||
|
||||
// A Stream is a HTTP/3 stream.
|
||||
// When writing to and reading from the stream, data is framed in HTTP/3 DATA frames.
|
||||
type Stream quic.Stream
|
||||
|
||||
// The stream conforms to the quic.Stream interface, but instead of writing to and reading directly
|
||||
// from the QUIC stream, it writes to and reads from the HTTP stream.
|
||||
type stream struct {
|
||||
quic.Stream
|
||||
|
||||
buf []byte
|
||||
|
||||
onFrameError func()
|
||||
bytesRemainingInFrame uint64
|
||||
}
|
||||
|
||||
var _ Stream = &stream{}
|
||||
|
||||
func newStream(str quic.Stream, onFrameError func()) *stream {
|
||||
return &stream{
|
||||
Stream: str,
|
||||
onFrameError: onFrameError,
|
||||
buf: make([]byte, 0, 16),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stream) Read(b []byte) (int, error) {
|
||||
if s.bytesRemainingInFrame == 0 {
|
||||
parseLoop:
|
||||
for {
|
||||
frame, err := parseNextFrame(s.Stream, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch f := frame.(type) {
|
||||
case *headersFrame:
|
||||
// skip HEADERS frames
|
||||
continue
|
||||
case *dataFrame:
|
||||
s.bytesRemainingInFrame = f.Length
|
||||
break parseLoop
|
||||
default:
|
||||
s.onFrameError()
|
||||
// parseNextFrame skips over unknown frame types
|
||||
// Therefore, this condition is only entered when we parsed another known frame type.
|
||||
return 0, fmt.Errorf("peer sent an unexpected frame: %T", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var n int
|
||||
var err error
|
||||
if s.bytesRemainingInFrame < uint64(len(b)) {
|
||||
n, err = s.Stream.Read(b[:s.bytesRemainingInFrame])
|
||||
} else {
|
||||
n, err = s.Stream.Read(b)
|
||||
}
|
||||
s.bytesRemainingInFrame -= uint64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *stream) hasMoreData() bool {
|
||||
return s.bytesRemainingInFrame > 0
|
||||
}
|
||||
|
||||
func (s *stream) Write(b []byte) (int, error) {
|
||||
s.buf = s.buf[:0]
|
||||
s.buf = (&dataFrame{Length: uint64(len(b))}).Append(s.buf)
|
||||
if _, err := s.Stream.Write(s.buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.Stream.Write(b)
|
||||
}
|
||||
|
||||
var errTooMuchData = errors.New("peer sent too much data")
|
||||
|
||||
type lengthLimitedStream struct {
|
||||
*stream
|
||||
contentLength int64
|
||||
read int64
|
||||
resetStream bool
|
||||
}
|
||||
|
||||
var _ Stream = &lengthLimitedStream{}
|
||||
|
||||
func newLengthLimitedStream(str *stream, contentLength int64) *lengthLimitedStream {
|
||||
return &lengthLimitedStream{
|
||||
stream: str,
|
||||
contentLength: contentLength,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *lengthLimitedStream) checkContentLengthViolation() error {
|
||||
if s.read > s.contentLength || s.read == s.contentLength && s.hasMoreData() {
|
||||
if !s.resetStream {
|
||||
s.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
s.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
s.resetStream = true
|
||||
}
|
||||
return errTooMuchData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *lengthLimitedStream) Read(b []byte) (int, error) {
|
||||
if err := s.checkContentLengthViolation(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := s.stream.Read(b[:utils.Min(int64(len(b)), s.contentLength-s.read)])
|
||||
s.read += int64(n)
|
||||
if err := s.checkContentLengthViolation(); err != nil {
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
203
http3/http_stream_test.go
Normal file
203
http3/http_stream_test.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func getDataFrame(data []byte) []byte {
|
||||
b := (&dataFrame{Length: uint64(len(data))}).Append(nil)
|
||||
return append(b, data...)
|
||||
}
|
||||
|
||||
var _ = Describe("Stream", func() {
|
||||
Context("reading", func() {
|
||||
var (
|
||||
str Stream
|
||||
qstr *mockquic.MockStream
|
||||
buf *bytes.Buffer
|
||||
errorCbCalled bool
|
||||
)
|
||||
|
||||
errorCb := func() { errorCbCalled = true }
|
||||
|
||||
BeforeEach(func() {
|
||||
buf = &bytes.Buffer{}
|
||||
errorCbCalled = false
|
||||
qstr = mockquic.NewMockStream(mockCtrl)
|
||||
qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes()
|
||||
qstr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes()
|
||||
str = newStream(qstr, errorCb)
|
||||
})
|
||||
|
||||
It("reads DATA frames in a single run", func() {
|
||||
buf.Write(getDataFrame([]byte("foobar")))
|
||||
b := make([]byte, 6)
|
||||
n, err := str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(b).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("reads DATA frames in multiple runs", func() {
|
||||
buf.Write(getDataFrame([]byte("foobar")))
|
||||
b := make([]byte, 3)
|
||||
n, err := str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(3))
|
||||
Expect(b).To(Equal([]byte("foo")))
|
||||
n, err = str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(3))
|
||||
Expect(b).To(Equal([]byte("bar")))
|
||||
})
|
||||
|
||||
It("reads DATA frames into too large buffers", func() {
|
||||
buf.Write(getDataFrame([]byte("foobar")))
|
||||
b := make([]byte, 10)
|
||||
n, err := str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(b[:n]).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("reads DATA frames into too large buffers, in multiple runs", func() {
|
||||
buf.Write(getDataFrame([]byte("foobar")))
|
||||
b := make([]byte, 4)
|
||||
n, err := str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(4))
|
||||
Expect(b).To(Equal([]byte("foob")))
|
||||
n, err = str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(2))
|
||||
Expect(b[:n]).To(Equal([]byte("ar")))
|
||||
})
|
||||
|
||||
It("reads multiple DATA frames", func() {
|
||||
buf.Write(getDataFrame([]byte("foo")))
|
||||
buf.Write(getDataFrame([]byte("bar")))
|
||||
b := make([]byte, 6)
|
||||
n, err := str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(3))
|
||||
Expect(b[:n]).To(Equal([]byte("foo")))
|
||||
n, err = str.Read(b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(3))
|
||||
Expect(b[:n]).To(Equal([]byte("bar")))
|
||||
})
|
||||
|
||||
It("skips HEADERS frames", func() {
|
||||
b := getDataFrame([]byte("foo"))
|
||||
b = (&headersFrame{Length: 10}).Append(b)
|
||||
b = append(b, make([]byte, 10)...)
|
||||
b = append(b, getDataFrame([]byte("bar"))...)
|
||||
buf.Write(b)
|
||||
r := make([]byte, 6)
|
||||
n, err := io.ReadFull(str, r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(r).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("errors when it can't parse the frame", func() {
|
||||
buf.Write([]byte("invalid"))
|
||||
_, err := str.Read([]byte{0})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("errors on unexpected frames, and calls the error callback", func() {
|
||||
b := (&settingsFrame{}).Append(nil)
|
||||
buf.Write(b)
|
||||
_, err := str.Read([]byte{0})
|
||||
Expect(err).To(MatchError("peer sent an unexpected frame: *http3.settingsFrame"))
|
||||
Expect(errorCbCalled).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("writing", func() {
|
||||
It("writes data frames", func() {
|
||||
buf := &bytes.Buffer{}
|
||||
qstr := mockquic.NewMockStream(mockCtrl)
|
||||
qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes()
|
||||
str := newStream(qstr, nil)
|
||||
str.Write([]byte("foo"))
|
||||
str.Write([]byte("foobar"))
|
||||
|
||||
f, err := parseNextFrame(buf, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(f).To(Equal(&dataFrame{Length: 3}))
|
||||
b := make([]byte, 3)
|
||||
_, err = io.ReadFull(buf, b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(b).To(Equal([]byte("foo")))
|
||||
|
||||
f, err = parseNextFrame(buf, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(f).To(Equal(&dataFrame{Length: 6}))
|
||||
b = make([]byte, 6)
|
||||
_, err = io.ReadFull(buf, b)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(b).To(Equal([]byte("foobar")))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("length-limited streams", func() {
|
||||
var (
|
||||
str *stream
|
||||
qstr *mockquic.MockStream
|
||||
buf *bytes.Buffer
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
buf = &bytes.Buffer{}
|
||||
qstr = mockquic.NewMockStream(mockCtrl)
|
||||
qstr.EXPECT().Write(gomock.Any()).DoAndReturn(buf.Write).AnyTimes()
|
||||
qstr.EXPECT().Read(gomock.Any()).DoAndReturn(buf.Read).AnyTimes()
|
||||
str = newStream(qstr, func() { Fail("didn't expect error callback to be called") })
|
||||
})
|
||||
|
||||
It("reads all frames", func() {
|
||||
s := newLengthLimitedStream(str, 6)
|
||||
buf.Write(getDataFrame([]byte("foo")))
|
||||
buf.Write(getDataFrame([]byte("bar")))
|
||||
data, err := io.ReadAll(s)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(data).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("errors if more data than the maximum length is sent, in the middle of a frame", func() {
|
||||
s := newLengthLimitedStream(str, 4)
|
||||
buf.Write(getDataFrame([]byte("foo")))
|
||||
buf.Write(getDataFrame([]byte("bar")))
|
||||
qstr.EXPECT().CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
qstr.EXPECT().CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
data, err := io.ReadAll(s)
|
||||
Expect(err).To(MatchError(errTooMuchData))
|
||||
Expect(data).To(Equal([]byte("foob")))
|
||||
// check that repeated calls to Read also return the right error
|
||||
n, err := s.Read([]byte{0})
|
||||
Expect(n).To(BeZero())
|
||||
Expect(err).To(MatchError(errTooMuchData))
|
||||
})
|
||||
|
||||
It("errors if more data than the maximum length is sent, as an additional frame", func() {
|
||||
s := newLengthLimitedStream(str, 3)
|
||||
buf.Write(getDataFrame([]byte("foo")))
|
||||
buf.Write(getDataFrame([]byte("bar")))
|
||||
qstr.EXPECT().CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
qstr.EXPECT().CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
|
||||
data, err := io.ReadAll(s)
|
||||
Expect(err).To(MatchError(errTooMuchData))
|
||||
Expect(data).To(Equal([]byte("foo")))
|
||||
})
|
||||
})
|
80
http3/mock_quic_early_listener_test.go
Normal file
80
http3/mock_quic_early_listener_test.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/quic-go/quic-go/http3 (interfaces: QUICEarlyListener)
|
||||
|
||||
// Package http3 is a generated GoMock package.
|
||||
package http3
|
||||
|
||||
import (
|
||||
context "context"
|
||||
net "net"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
quic "github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
// MockQUICEarlyListener is a mock of QUICEarlyListener interface.
|
||||
type MockQUICEarlyListener struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQUICEarlyListenerMockRecorder
|
||||
}
|
||||
|
||||
// MockQUICEarlyListenerMockRecorder is the mock recorder for MockQUICEarlyListener.
|
||||
type MockQUICEarlyListenerMockRecorder struct {
|
||||
mock *MockQUICEarlyListener
|
||||
}
|
||||
|
||||
// NewMockQUICEarlyListener creates a new mock instance.
|
||||
func NewMockQUICEarlyListener(ctrl *gomock.Controller) *MockQUICEarlyListener {
|
||||
mock := &MockQUICEarlyListener{ctrl: ctrl}
|
||||
mock.recorder = &MockQUICEarlyListenerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQUICEarlyListener) EXPECT() *MockQUICEarlyListenerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Accept mocks base method.
|
||||
func (m *MockQUICEarlyListener) Accept(arg0 context.Context) (quic.EarlyConnection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Accept", arg0)
|
||||
ret0, _ := ret[0].(quic.EarlyConnection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Accept indicates an expected call of Accept.
|
||||
func (mr *MockQUICEarlyListenerMockRecorder) Accept(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accept", reflect.TypeOf((*MockQUICEarlyListener)(nil).Accept), arg0)
|
||||
}
|
||||
|
||||
// Addr mocks base method.
|
||||
func (m *MockQUICEarlyListener) Addr() net.Addr {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Addr")
|
||||
ret0, _ := ret[0].(net.Addr)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Addr indicates an expected call of Addr.
|
||||
func (mr *MockQUICEarlyListenerMockRecorder) Addr() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addr", reflect.TypeOf((*MockQUICEarlyListener)(nil).Addr))
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockQUICEarlyListener) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockQUICEarlyListenerMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockQUICEarlyListener)(nil).Close))
|
||||
}
|
78
http3/mock_roundtripcloser_test.go
Normal file
78
http3/mock_roundtripcloser_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/quic-go/quic-go/http3 (interfaces: RoundTripCloser)
|
||||
|
||||
// Package http3 is a generated GoMock package.
|
||||
package http3
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockRoundTripCloser is a mock of RoundTripCloser interface.
|
||||
type MockRoundTripCloser struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRoundTripCloserMockRecorder
|
||||
}
|
||||
|
||||
// MockRoundTripCloserMockRecorder is the mock recorder for MockRoundTripCloser.
|
||||
type MockRoundTripCloserMockRecorder struct {
|
||||
mock *MockRoundTripCloser
|
||||
}
|
||||
|
||||
// NewMockRoundTripCloser creates a new mock instance.
|
||||
func NewMockRoundTripCloser(ctrl *gomock.Controller) *MockRoundTripCloser {
|
||||
mock := &MockRoundTripCloser{ctrl: ctrl}
|
||||
mock.recorder = &MockRoundTripCloserMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRoundTripCloser) EXPECT() *MockRoundTripCloserMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockRoundTripCloser) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockRoundTripCloserMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRoundTripCloser)(nil).Close))
|
||||
}
|
||||
|
||||
// HandshakeComplete mocks base method.
|
||||
func (m *MockRoundTripCloser) HandshakeComplete() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandshakeComplete")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HandshakeComplete indicates an expected call of HandshakeComplete.
|
||||
func (mr *MockRoundTripCloserMockRecorder) HandshakeComplete() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandshakeComplete", reflect.TypeOf((*MockRoundTripCloser)(nil).HandshakeComplete))
|
||||
}
|
||||
|
||||
// RoundTripOpt mocks base method.
|
||||
func (m *MockRoundTripCloser) RoundTripOpt(arg0 *http.Request, arg1 RoundTripOpt) (*http.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RoundTripOpt", arg0, arg1)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RoundTripOpt indicates an expected call of RoundTripOpt.
|
||||
func (mr *MockRoundTripCloserMockRecorder) RoundTripOpt(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoundTripOpt", reflect.TypeOf((*MockRoundTripCloser)(nil).RoundTripOpt), arg0, arg1)
|
||||
}
|
8
http3/mockgen.go
Normal file
8
http3/mockgen.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
//go:build gomock || generate
|
||||
|
||||
package http3
|
||||
|
||||
//go:generate sh -c "go run github.com/golang/mock/mockgen -build_flags=\"-tags=gomock\" -package http3 -destination mock_roundtripcloser_test.go github.com/quic-go/quic-go/http3 RoundTripCloser"
|
||||
type RoundTripCloser = roundTripCloser
|
||||
|
||||
//go:generate sh -c "go run github.com/golang/mock/mockgen -package http3 -destination mock_quic_early_listener_test.go github.com/quic-go/quic-go/http3 QUICEarlyListener"
|
287
http3/request_writer.go
Normal file
287
http3/request_writer.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/quic-go/qpack"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
)
|
||||
|
||||
const bodyCopyBufferSize = 8 * 1024
|
||||
|
||||
type requestWriter struct {
|
||||
mutex sync.Mutex
|
||||
encoder *qpack.Encoder
|
||||
headerBuf *bytes.Buffer
|
||||
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
func newRequestWriter(logger utils.Logger) *requestWriter {
|
||||
headerBuf := &bytes.Buffer{}
|
||||
encoder := qpack.NewEncoder(headerBuf)
|
||||
return &requestWriter{
|
||||
encoder: encoder,
|
||||
headerBuf: headerBuf,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *requestWriter) WriteRequestHeader(str quic.Stream, req *http.Request, gzip bool) error {
|
||||
// TODO: figure out how to add support for trailers
|
||||
buf := &bytes.Buffer{}
|
||||
if err := w.writeHeaders(buf, req, gzip); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := str.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool) error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
defer w.encoder.Close()
|
||||
defer w.headerBuf.Reset()
|
||||
|
||||
if err := w.encodeHeaders(req, gzip, "", actualContentLength(req)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := make([]byte, 0, 128)
|
||||
b = (&headersFrame{Length: uint64(w.headerBuf.Len())}).Append(b)
|
||||
if _, err := wr.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := wr.Write(w.headerBuf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// copied from net/transport.go
|
||||
// Modified to support Extended CONNECT:
|
||||
// Contrary to what the godoc for the http.Request says,
|
||||
// we do respect the Proto field if the method is CONNECT.
|
||||
func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) error {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
host, err := httpguts.PunycodeHostPort(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !httpguts.ValidHostHeader(host) {
|
||||
return errors.New("http3: invalid Host header")
|
||||
}
|
||||
|
||||
// http.NewRequest sets this field to HTTP/1.1
|
||||
isExtendedConnect := req.Method == http.MethodConnect && req.Proto != "" && req.Proto != "HTTP/1.1"
|
||||
|
||||
var path string
|
||||
if req.Method != http.MethodConnect || isExtendedConnect {
|
||||
path = req.URL.RequestURI()
|
||||
if !validPseudoPath(path) {
|
||||
orig := path
|
||||
path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
|
||||
if !validPseudoPath(path) {
|
||||
if req.URL.Opaque != "" {
|
||||
return fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
|
||||
} else {
|
||||
return fmt.Errorf("invalid request :path %q", orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any invalid headers and return an error before we
|
||||
// potentially pollute our hpack state. (We want to be able to
|
||||
// continue to reuse the hpack encoder for future requests)
|
||||
for k, vv := range req.Header {
|
||||
if !httpguts.ValidHeaderFieldName(k) {
|
||||
return fmt.Errorf("invalid HTTP header name %q", k)
|
||||
}
|
||||
for _, v := range vv {
|
||||
if !httpguts.ValidHeaderFieldValue(v) {
|
||||
return fmt.Errorf("invalid HTTP header value %q for header %q", v, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enumerateHeaders := func(f func(name, value string)) {
|
||||
// 8.1.2.3 Request Pseudo-Header Fields
|
||||
// The :path pseudo-header field includes the path and query parts of the
|
||||
// target URI (the path-absolute production and optionally a '?' character
|
||||
// followed by the query production (see Sections 3.3 and 3.4 of
|
||||
// [RFC3986]).
|
||||
f(":authority", host)
|
||||
f(":method", req.Method)
|
||||
if req.Method != http.MethodConnect || isExtendedConnect {
|
||||
f(":path", path)
|
||||
f(":scheme", req.URL.Scheme)
|
||||
}
|
||||
if isExtendedConnect {
|
||||
f(":protocol", req.Proto)
|
||||
}
|
||||
if trailers != "" {
|
||||
f("trailer", trailers)
|
||||
}
|
||||
|
||||
var didUA bool
|
||||
for k, vv := range req.Header {
|
||||
if strings.EqualFold(k, "host") || strings.EqualFold(k, "content-length") {
|
||||
// Host is :authority, already sent.
|
||||
// Content-Length is automatic, set below.
|
||||
continue
|
||||
} else if strings.EqualFold(k, "connection") || strings.EqualFold(k, "proxy-connection") ||
|
||||
strings.EqualFold(k, "transfer-encoding") || strings.EqualFold(k, "upgrade") ||
|
||||
strings.EqualFold(k, "keep-alive") {
|
||||
// Per 8.1.2.2 Connection-Specific Header
|
||||
// Fields, don't send connection-specific
|
||||
// fields. We have already checked if any
|
||||
// are error-worthy so just ignore the rest.
|
||||
continue
|
||||
} else if strings.EqualFold(k, "user-agent") {
|
||||
// Match Go's http1 behavior: at most one
|
||||
// User-Agent. If set to nil or empty string,
|
||||
// then omit it. Otherwise if not mentioned,
|
||||
// include the default (below).
|
||||
didUA = true
|
||||
if len(vv) < 1 {
|
||||
continue
|
||||
}
|
||||
vv = vv[:1]
|
||||
if vv[0] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, v := range vv {
|
||||
f(k, v)
|
||||
}
|
||||
}
|
||||
if shouldSendReqContentLength(req.Method, contentLength) {
|
||||
f("content-length", strconv.FormatInt(contentLength, 10))
|
||||
}
|
||||
if addGzipHeader {
|
||||
f("accept-encoding", "gzip")
|
||||
}
|
||||
if !didUA {
|
||||
f("user-agent", defaultUserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
// Do a first pass over the headers counting bytes to ensure
|
||||
// we don't exceed cc.peerMaxHeaderListSize. This is done as a
|
||||
// separate pass before encoding the headers to prevent
|
||||
// modifying the hpack state.
|
||||
hlSize := uint64(0)
|
||||
enumerateHeaders(func(name, value string) {
|
||||
hf := hpack.HeaderField{Name: name, Value: value}
|
||||
hlSize += uint64(hf.Size())
|
||||
})
|
||||
|
||||
// TODO: check maximum header list size
|
||||
// if hlSize > cc.peerMaxHeaderListSize {
|
||||
// return errRequestHeaderListSize
|
||||
// }
|
||||
|
||||
// trace := httptrace.ContextClientTrace(req.Context())
|
||||
// traceHeaders := traceHasWroteHeaderField(trace)
|
||||
|
||||
// Header list size is ok. Write the headers.
|
||||
enumerateHeaders(func(name, value string) {
|
||||
name = strings.ToLower(name)
|
||||
w.encoder.WriteField(qpack.HeaderField{Name: name, Value: value})
|
||||
// if traceHeaders {
|
||||
// traceWroteHeaderField(trace, name, value)
|
||||
// }
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
|
||||
// and returns a host:port. The port 443 is added if needed.
|
||||
func authorityAddr(scheme string, authority string) (addr string) {
|
||||
host, port, err := net.SplitHostPort(authority)
|
||||
if err != nil { // authority didn't have a port
|
||||
port = "443"
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
}
|
||||
host = authority
|
||||
}
|
||||
if a, err := idna.ToASCII(host); err == nil {
|
||||
host = a
|
||||
}
|
||||
// IPv6 address literal, without a port:
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
return host + ":" + port
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// validPseudoPath reports whether v is a valid :path pseudo-header
|
||||
// value. It must be either:
|
||||
//
|
||||
// *) a non-empty string starting with '/'
|
||||
// *) the string '*', for OPTIONS requests.
|
||||
//
|
||||
// For now this is only used a quick check for deciding when to clean
|
||||
// up Opaque URLs before sending requests from the Transport.
|
||||
// See golang.org/issue/16847
|
||||
//
|
||||
// We used to enforce that the path also didn't start with "//", but
|
||||
// Google's GFE accepts such paths and Chrome sends them, so ignore
|
||||
// that part of the spec. See golang.org/issue/19103.
|
||||
func validPseudoPath(v string) bool {
|
||||
return (len(v) > 0 && v[0] == '/') || v == "*"
|
||||
}
|
||||
|
||||
// actualContentLength returns a sanitized version of
|
||||
// req.ContentLength, where 0 actually means zero (not unknown) and -1
|
||||
// means unknown.
|
||||
func actualContentLength(req *http.Request) int64 {
|
||||
if req.Body == nil {
|
||||
return 0
|
||||
}
|
||||
if req.ContentLength != 0 {
|
||||
return req.ContentLength
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// shouldSendReqContentLength reports whether the http2.Transport should send
|
||||
// a "content-length" request header. This logic is basically a copy of the net/http
|
||||
// transferWriter.shouldSendContentLength.
|
||||
// The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).
|
||||
// -1 means unknown.
|
||||
func shouldSendReqContentLength(method string, contentLength int64) bool {
|
||||
if contentLength > 0 {
|
||||
return true
|
||||
}
|
||||
if contentLength < 0 {
|
||||
return false
|
||||
}
|
||||
// For zero bodies, whether we send a content-length depends on the method.
|
||||
// It also kinda doesn't matter for http2 either way, with END_STREAM.
|
||||
switch method {
|
||||
case "POST", "PUT", "PATCH":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
119
http3/request_writer_test.go
Normal file
119
http3/request_writer_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/quic-go/qpack"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Request Writer", func() {
|
||||
var (
|
||||
rw *requestWriter
|
||||
str *mockquic.MockStream
|
||||
strBuf *bytes.Buffer
|
||||
)
|
||||
|
||||
decode := func(str io.Reader) map[string]string {
|
||||
frame, err := parseNextFrame(str, nil)
|
||||
ExpectWithOffset(1, err).ToNot(HaveOccurred())
|
||||
ExpectWithOffset(1, frame).To(BeAssignableToTypeOf(&headersFrame{}))
|
||||
headersFrame := frame.(*headersFrame)
|
||||
data := make([]byte, headersFrame.Length)
|
||||
_, err = io.ReadFull(str, data)
|
||||
ExpectWithOffset(1, err).ToNot(HaveOccurred())
|
||||
decoder := qpack.NewDecoder(nil)
|
||||
hfs, err := decoder.DecodeFull(data)
|
||||
ExpectWithOffset(1, err).ToNot(HaveOccurred())
|
||||
values := make(map[string]string)
|
||||
for _, hf := range hfs {
|
||||
values[hf.Name] = hf.Value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
rw = newRequestWriter(utils.DefaultLogger)
|
||||
strBuf = &bytes.Buffer{}
|
||||
str = mockquic.NewMockStream(mockCtrl)
|
||||
str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
|
||||
})
|
||||
|
||||
It("writes a GET request", func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/index.html?foo=bar", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
|
||||
headerFields := decode(strBuf)
|
||||
Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":method", "GET"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":path", "/index.html?foo=bar"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":scheme", "https"))
|
||||
Expect(headerFields).ToNot(HaveKey("accept-encoding"))
|
||||
})
|
||||
|
||||
It("rejects invalid host headers", func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/index.html?foo=bar", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
req.Host = "foo@bar" // @ is invalid
|
||||
Expect(rw.WriteRequestHeader(str, req, false)).To(MatchError("http3: invalid Host header"))
|
||||
})
|
||||
|
||||
It("sends cookies", func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cookie1 := &http.Cookie{
|
||||
Name: "Cookie #1",
|
||||
Value: "Value #1",
|
||||
}
|
||||
cookie2 := &http.Cookie{
|
||||
Name: "Cookie #2",
|
||||
Value: "Value #2",
|
||||
}
|
||||
req.AddCookie(cookie1)
|
||||
req.AddCookie(cookie2)
|
||||
Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
|
||||
headerFields := decode(strBuf)
|
||||
Expect(headerFields).To(HaveKeyWithValue("cookie", `Cookie #1="Value #1"; Cookie #2="Value #2"`))
|
||||
})
|
||||
|
||||
It("adds the header for gzip support", func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://quic.clemente.io/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rw.WriteRequestHeader(str, req, true)).To(Succeed())
|
||||
headerFields := decode(strBuf)
|
||||
Expect(headerFields).To(HaveKeyWithValue("accept-encoding", "gzip"))
|
||||
})
|
||||
|
||||
It("writes a CONNECT request", func() {
|
||||
req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
|
||||
headerFields := decode(strBuf)
|
||||
Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
|
||||
Expect(headerFields).ToNot(HaveKey(":path"))
|
||||
Expect(headerFields).ToNot(HaveKey(":scheme"))
|
||||
Expect(headerFields).ToNot(HaveKey(":protocol"))
|
||||
})
|
||||
|
||||
It("writes an Extended CONNECT request", func() {
|
||||
req, err := http.NewRequest(http.MethodConnect, "https://quic.clemente.io/foobar", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
req.Proto = "webtransport"
|
||||
Expect(rw.WriteRequestHeader(str, req, false)).To(Succeed())
|
||||
headerFields := decode(strBuf)
|
||||
Expect(headerFields).To(HaveKeyWithValue(":authority", "quic.clemente.io"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":method", "CONNECT"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":path", "/foobar"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":scheme", "https"))
|
||||
Expect(headerFields).To(HaveKeyWithValue(":protocol", "webtransport"))
|
||||
})
|
||||
})
|
183
http3/response_writer.go
Normal file
183
http3/response_writer.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
|
||||
"github.com/quic-go/qpack"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
conn quic.Connection
|
||||
str quic.Stream
|
||||
bufferedStr *bufio.Writer
|
||||
buf []byte
|
||||
|
||||
header http.Header
|
||||
status int // status code passed to WriteHeader
|
||||
headerWritten bool
|
||||
contentLen int64 // if handler set valid Content-Length header
|
||||
numWritten int64 // bytes written
|
||||
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
_ http.ResponseWriter = &responseWriter{}
|
||||
_ http.Flusher = &responseWriter{}
|
||||
_ Hijacker = &responseWriter{}
|
||||
)
|
||||
|
||||
func newResponseWriter(str quic.Stream, conn quic.Connection, logger utils.Logger) *responseWriter {
|
||||
return &responseWriter{
|
||||
header: http.Header{},
|
||||
buf: make([]byte, 16),
|
||||
conn: conn,
|
||||
str: str,
|
||||
bufferedStr: bufio.NewWriter(str),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(status int) {
|
||||
if w.headerWritten {
|
||||
return
|
||||
}
|
||||
|
||||
// http status must be 3 digits
|
||||
if status < 100 || status > 999 {
|
||||
panic(fmt.Sprintf("invalid WriteHeader code %v", status))
|
||||
}
|
||||
|
||||
if status >= 200 {
|
||||
w.headerWritten = true
|
||||
// Add Date header.
|
||||
// This is what the standard library does.
|
||||
// Can be disabled by setting the Date header to nil.
|
||||
if _, ok := w.header["Date"]; !ok {
|
||||
w.header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
|
||||
}
|
||||
// Content-Length checking
|
||||
// use ParseUint instead of ParseInt, as negative values are invalid
|
||||
if clen := w.header.Get("Content-Length"); clen != "" {
|
||||
if cl, err := strconv.ParseUint(clen, 10, 63); err == nil {
|
||||
w.contentLen = int64(cl)
|
||||
} else {
|
||||
// emit a warning for malformed Content-Length and remove it
|
||||
w.logger.Errorf("Malformed Content-Length %s", clen)
|
||||
w.header.Del("Content-Length")
|
||||
}
|
||||
}
|
||||
}
|
||||
w.status = status
|
||||
|
||||
var headers bytes.Buffer
|
||||
enc := qpack.NewEncoder(&headers)
|
||||
enc.WriteField(qpack.HeaderField{Name: ":status", Value: strconv.Itoa(status)})
|
||||
|
||||
for k, v := range w.header {
|
||||
for index := range v {
|
||||
enc.WriteField(qpack.HeaderField{Name: strings.ToLower(k), Value: v[index]})
|
||||
}
|
||||
}
|
||||
|
||||
w.buf = w.buf[:0]
|
||||
w.buf = (&headersFrame{Length: uint64(headers.Len())}).Append(w.buf)
|
||||
w.logger.Infof("Responding with %d", status)
|
||||
if _, err := w.bufferedStr.Write(w.buf); err != nil {
|
||||
w.logger.Errorf("could not write headers frame: %s", err.Error())
|
||||
}
|
||||
if _, err := w.bufferedStr.Write(headers.Bytes()); err != nil {
|
||||
w.logger.Errorf("could not write header frame payload: %s", err.Error())
|
||||
}
|
||||
if !w.headerWritten {
|
||||
w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(p []byte) (int, error) {
|
||||
bodyAllowed := bodyAllowedForStatus(w.status)
|
||||
if !w.headerWritten {
|
||||
// If body is not allowed, we don't need to (and we can't) sniff the content type.
|
||||
if bodyAllowed {
|
||||
// If no content type, apply sniffing algorithm to body.
|
||||
// We can't use `w.header.Get` here since if the Content-Type was set to nil, we shoundn't do sniffing.
|
||||
_, haveType := w.header["Content-Type"]
|
||||
|
||||
// If the Transfer-Encoding or Content-Encoding was set and is non-blank,
|
||||
// we shouldn't sniff the body.
|
||||
hasTE := w.header.Get("Transfer-Encoding") != ""
|
||||
hasCE := w.header.Get("Content-Encoding") != ""
|
||||
if !hasCE && !haveType && !hasTE && len(p) > 0 {
|
||||
w.header.Set("Content-Type", http.DetectContentType(p))
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
bodyAllowed = true
|
||||
}
|
||||
if !bodyAllowed {
|
||||
return 0, http.ErrBodyNotAllowed
|
||||
}
|
||||
|
||||
w.numWritten += int64(len(p))
|
||||
if w.contentLen != 0 && w.numWritten > w.contentLen {
|
||||
return 0, http.ErrContentLength
|
||||
}
|
||||
|
||||
df := &dataFrame{Length: uint64(len(p))}
|
||||
w.buf = w.buf[:0]
|
||||
w.buf = df.Append(w.buf)
|
||||
if _, err := w.bufferedStr.Write(w.buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.bufferedStr.Write(p)
|
||||
}
|
||||
|
||||
func (w *responseWriter) FlushError() error {
|
||||
return w.bufferedStr.Flush()
|
||||
}
|
||||
|
||||
func (w *responseWriter) Flush() {
|
||||
if err := w.FlushError(); err != nil {
|
||||
w.logger.Errorf("could not flush to stream: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) StreamCreator() StreamCreator {
|
||||
return w.conn
|
||||
}
|
||||
|
||||
func (w *responseWriter) SetReadDeadline(deadline time.Time) error {
|
||||
return w.str.SetReadDeadline(deadline)
|
||||
}
|
||||
|
||||
func (w *responseWriter) SetWriteDeadline(deadline time.Time) error {
|
||||
return w.str.SetWriteDeadline(deadline)
|
||||
}
|
||||
|
||||
// copied from http2/http2.go
|
||||
// bodyAllowedForStatus reports whether a given response status code
|
||||
// permits a body. See RFC 2616, section 4.4.
|
||||
func bodyAllowedForStatus(status int) bool {
|
||||
switch {
|
||||
case status >= 100 && status <= 199:
|
||||
return false
|
||||
case status == http.StatusNoContent:
|
||||
return false
|
||||
case status == http.StatusNotModified:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
186
http3/response_writer_test.go
Normal file
186
http3/response_writer_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
mockquic "github.com/quic-go/quic-go/internal/mocks/quic"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/quic-go/qpack"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Response Writer", func() {
|
||||
var (
|
||||
rw *responseWriter
|
||||
strBuf *bytes.Buffer
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
strBuf = &bytes.Buffer{}
|
||||
str := mockquic.NewMockStream(mockCtrl)
|
||||
str.EXPECT().Write(gomock.Any()).DoAndReturn(strBuf.Write).AnyTimes()
|
||||
str.EXPECT().SetReadDeadline(gomock.Any()).Return(nil).AnyTimes()
|
||||
str.EXPECT().SetWriteDeadline(gomock.Any()).Return(nil).AnyTimes()
|
||||
rw = newResponseWriter(str, nil, utils.DefaultLogger)
|
||||
})
|
||||
|
||||
decodeHeader := func(str io.Reader) map[string][]string {
|
||||
rw.Flush()
|
||||
fields := make(map[string][]string)
|
||||
decoder := qpack.NewDecoder(nil)
|
||||
|
||||
frame, err := parseNextFrame(str, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&headersFrame{}))
|
||||
headersFrame := frame.(*headersFrame)
|
||||
data := make([]byte, headersFrame.Length)
|
||||
_, err = io.ReadFull(str, data)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
hfs, err := decoder.DecodeFull(data)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
for _, p := range hfs {
|
||||
fields[p.Name] = append(fields[p.Name], p.Value)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
getData := func(str io.Reader) []byte {
|
||||
frame, err := parseNextFrame(str, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(frame).To(BeAssignableToTypeOf(&dataFrame{}))
|
||||
df := frame.(*dataFrame)
|
||||
data := make([]byte, df.Length)
|
||||
_, err = io.ReadFull(str, data)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return data
|
||||
}
|
||||
|
||||
It("writes status", func() {
|
||||
rw.WriteHeader(http.StatusTeapot)
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveLen(2))
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"418"}))
|
||||
Expect(fields).To(HaveKey("date"))
|
||||
})
|
||||
|
||||
It("writes headers", func() {
|
||||
rw.Header().Add("content-length", "42")
|
||||
rw.WriteHeader(http.StatusTeapot)
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveKeyWithValue("content-length", []string{"42"}))
|
||||
})
|
||||
|
||||
It("writes multiple headers with the same name", func() {
|
||||
const cookie1 = "test1=1; Max-Age=7200; path=/"
|
||||
const cookie2 = "test2=2; Max-Age=7200; path=/"
|
||||
rw.Header().Add("set-cookie", cookie1)
|
||||
rw.Header().Add("set-cookie", cookie2)
|
||||
rw.WriteHeader(http.StatusTeapot)
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveKey("set-cookie"))
|
||||
cookies := fields["set-cookie"]
|
||||
Expect(cookies).To(ContainElement(cookie1))
|
||||
Expect(cookies).To(ContainElement(cookie2))
|
||||
})
|
||||
|
||||
It("writes data", func() {
|
||||
n, err := rw.Write([]byte("foobar"))
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Should have written 200 on the header stream
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"200"}))
|
||||
// And foobar on the data stream
|
||||
Expect(getData(strBuf)).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("writes data after WriteHeader is called", func() {
|
||||
rw.WriteHeader(http.StatusTeapot)
|
||||
n, err := rw.Write([]byte("foobar"))
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Should have written 418 on the header stream
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"418"}))
|
||||
// And foobar on the data stream
|
||||
Expect(getData(strBuf)).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("does not WriteHeader() twice", func() {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveLen(2))
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"200"}))
|
||||
Expect(fields).To(HaveKey("date"))
|
||||
})
|
||||
|
||||
It("allows calling WriteHeader() several times when using the 103 status code", func() {
|
||||
rw.Header().Add("Link", "</style.css>; rel=preload; as=style")
|
||||
rw.Header().Add("Link", "</script.js>; rel=preload; as=script")
|
||||
rw.WriteHeader(http.StatusEarlyHints)
|
||||
|
||||
n, err := rw.Write([]byte("foobar"))
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Early Hints must have been received
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveLen(2))
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"103"}))
|
||||
Expect(fields).To(HaveKeyWithValue("link", []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}))
|
||||
|
||||
// According to the spec, headers sent in the informational response must also be included in the final response
|
||||
fields = decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveLen(3))
|
||||
Expect(fields).To(HaveKeyWithValue(":status", []string{"200"}))
|
||||
Expect(fields).To(HaveKey("date"))
|
||||
Expect(fields).To(HaveKeyWithValue("link", []string{"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"}))
|
||||
|
||||
Expect(getData(strBuf)).To(Equal([]byte("foobar")))
|
||||
})
|
||||
|
||||
It("doesn't allow writes if the status code doesn't allow a body", func() {
|
||||
rw.WriteHeader(304)
|
||||
n, err := rw.Write([]byte("foobar"))
|
||||
Expect(n).To(BeZero())
|
||||
Expect(err).To(MatchError(http.ErrBodyNotAllowed))
|
||||
})
|
||||
|
||||
It("first call to Write sniffs if Content-Type is not set", func() {
|
||||
n, err := rw.Write([]byte("<html></html>"))
|
||||
Expect(n).To(Equal(13))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
fields := decodeHeader(strBuf)
|
||||
Expect(fields).To(HaveKeyWithValue("content-type", []string{"text/html; charset=utf-8"}))
|
||||
})
|
||||
|
||||
It(`is compatible with "net/http".ResponseController`, func() {
|
||||
Expect(rw.SetReadDeadline(time.Now().Add(1 * time.Second))).To(BeNil())
|
||||
Expect(rw.SetWriteDeadline(time.Now().Add(1 * time.Second))).To(BeNil())
|
||||
})
|
||||
|
||||
It(`checks Content-Length header`, func() {
|
||||
rw.Header().Set("Content-Length", "6")
|
||||
n, err := rw.Write([]byte("foobar"))
|
||||
Expect(n).To(Equal(6))
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
n, err = rw.Write([]byte("foobar"))
|
||||
Expect(n).To(Equal(0))
|
||||
Expect(err).To(Equal(http.ErrContentLength))
|
||||
})
|
||||
|
||||
It(`panics when writing invalid status`, func() {
|
||||
Expect(func() { rw.WriteHeader(99) }).To(Panic())
|
||||
Expect(func() { rw.WriteHeader(1000) }).To(Panic())
|
||||
})
|
||||
})
|
304
http3/roundtrip.go
Normal file
304
http3/roundtrip.go
Normal file
|
@ -0,0 +1,304 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type roundTripCloser interface {
|
||||
RoundTripOpt(*http.Request, RoundTripOpt) (*http.Response, error)
|
||||
HandshakeComplete() bool
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type roundTripCloserWithCount struct {
|
||||
roundTripCloser
|
||||
useCount atomic.Int64
|
||||
}
|
||||
|
||||
// RoundTripper implements the http.RoundTripper interface
|
||||
type RoundTripper struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
// DisableCompression, if true, prevents the Transport from
|
||||
// requesting compression with an "Accept-Encoding: gzip"
|
||||
// request header when the Request contains no existing
|
||||
// Accept-Encoding value. If the Transport requests gzip on
|
||||
// its own and gets a gzipped response, it's transparently
|
||||
// decoded in the Response.Body. However, if the user
|
||||
// explicitly requested gzip it is not automatically
|
||||
// uncompressed.
|
||||
DisableCompression bool
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with
|
||||
// tls.Client. If nil, the default configuration is used.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// QuicConfig is the quic.Config used for dialing new connections.
|
||||
// If nil, reasonable default values will be used.
|
||||
QuicConfig *quic.Config
|
||||
|
||||
// Enable support for HTTP/3 datagrams.
|
||||
// If set to true, QuicConfig.EnableDatagram will be set.
|
||||
// See https://www.ietf.org/archive/id/draft-schinazi-masque-h3-datagram-02.html.
|
||||
EnableDatagrams bool
|
||||
|
||||
// Additional HTTP/3 settings.
|
||||
// It is invalid to specify any settings defined by the HTTP/3 draft and the datagram draft.
|
||||
AdditionalSettings map[uint64]uint64
|
||||
|
||||
// When set, this callback is called for the first unknown frame parsed on a bidirectional stream.
|
||||
// It is called right after parsing the frame type.
|
||||
// If parsing the frame type fails, the error is passed to the callback.
|
||||
// In that case, the frame type will not be set.
|
||||
// Callers can either ignore the frame and return control of the stream back to HTTP/3
|
||||
// (by returning hijacked false).
|
||||
// Alternatively, callers can take over the QUIC stream (by returning hijacked true).
|
||||
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
|
||||
|
||||
// When set, this callback is called for unknown unidirectional stream of unknown stream type.
|
||||
// If parsing the stream type fails, the error is passed to the callback.
|
||||
// In that case, the stream type will not be set.
|
||||
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
|
||||
|
||||
// Dial specifies an optional dial function for creating QUIC
|
||||
// connections for requests.
|
||||
// If Dial is nil, a UDPConn will be created at the first request
|
||||
// and will be reused for subsequent connections to other servers.
|
||||
Dial func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error)
|
||||
|
||||
// MaxResponseHeaderBytes specifies a limit on how many response bytes are
|
||||
// allowed in the server's response header.
|
||||
// Zero means to use a default limit.
|
||||
MaxResponseHeaderBytes int64
|
||||
|
||||
newClient func(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) // so we can mock it in tests
|
||||
clients map[string]*roundTripCloserWithCount
|
||||
transport *quic.Transport
|
||||
}
|
||||
|
||||
// RoundTripOpt are options for the Transport.RoundTripOpt method.
|
||||
type RoundTripOpt struct {
|
||||
// OnlyCachedConn controls whether the RoundTripper may create a new QUIC connection.
|
||||
// If set true and no cached connection is available, RoundTripOpt will return ErrNoCachedConn.
|
||||
OnlyCachedConn bool
|
||||
// DontCloseRequestStream controls whether the request stream is closed after sending the request.
|
||||
// If set, context cancellations have no effect after the response headers are received.
|
||||
DontCloseRequestStream bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ http.RoundTripper = &RoundTripper{}
|
||||
_ io.Closer = &RoundTripper{}
|
||||
)
|
||||
|
||||
// ErrNoCachedConn is returned when RoundTripper.OnlyCachedConn is set
|
||||
var ErrNoCachedConn = errors.New("http3: no cached connection was available")
|
||||
|
||||
// RoundTripOpt is like RoundTrip, but takes options.
|
||||
func (r *RoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
if req.URL == nil {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: nil Request.URL")
|
||||
}
|
||||
if req.URL.Scheme != "https" {
|
||||
closeRequestBody(req)
|
||||
return nil, fmt.Errorf("http3: unsupported protocol scheme: %s", req.URL.Scheme)
|
||||
}
|
||||
if req.URL.Host == "" {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: no Host in request URL")
|
||||
}
|
||||
if req.Header == nil {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: nil Request.Header")
|
||||
}
|
||||
for k, vv := range req.Header {
|
||||
if !httpguts.ValidHeaderFieldName(k) {
|
||||
return nil, fmt.Errorf("http3: invalid http header field name %q", k)
|
||||
}
|
||||
for _, v := range vv {
|
||||
if !httpguts.ValidHeaderFieldValue(v) {
|
||||
return nil, fmt.Errorf("http3: invalid http header field value %q for key %v", v, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method != "" && !validMethod(req.Method) {
|
||||
closeRequestBody(req)
|
||||
return nil, fmt.Errorf("http3: invalid method %q", req.Method)
|
||||
}
|
||||
|
||||
hostname := authorityAddr("https", hostnameFromRequest(req))
|
||||
cl, isReused, err := r.getClient(hostname, opt.OnlyCachedConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cl.useCount.Add(-1)
|
||||
rsp, err := cl.RoundTripOpt(req, opt)
|
||||
if err != nil {
|
||||
r.removeClient(hostname)
|
||||
if isReused {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
return r.RoundTripOpt(req, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// RoundTrip does a round trip.
|
||||
func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return r.RoundTripOpt(req, RoundTripOpt{})
|
||||
}
|
||||
|
||||
func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc *roundTripCloserWithCount, isReused bool, err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.clients == nil {
|
||||
r.clients = make(map[string]*roundTripCloserWithCount)
|
||||
}
|
||||
|
||||
client, ok := r.clients[hostname]
|
||||
if !ok {
|
||||
if onlyCached {
|
||||
return nil, false, ErrNoCachedConn
|
||||
}
|
||||
var err error
|
||||
newCl := newClient
|
||||
if r.newClient != nil {
|
||||
newCl = r.newClient
|
||||
}
|
||||
dial := r.Dial
|
||||
if dial == nil {
|
||||
if r.transport == nil {
|
||||
udpConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
r.transport = &quic.Transport{
|
||||
Conn: udpConn,
|
||||
}
|
||||
}
|
||||
dial = r.makeDialer()
|
||||
}
|
||||
c, err := newCl(
|
||||
hostname,
|
||||
r.TLSClientConfig,
|
||||
&roundTripperOpts{
|
||||
EnableDatagram: r.EnableDatagrams,
|
||||
DisableCompression: r.DisableCompression,
|
||||
MaxHeaderBytes: r.MaxResponseHeaderBytes,
|
||||
StreamHijacker: r.StreamHijacker,
|
||||
UniStreamHijacker: r.UniStreamHijacker,
|
||||
},
|
||||
r.QuicConfig,
|
||||
dial,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
client = &roundTripCloserWithCount{roundTripCloser: c}
|
||||
r.clients[hostname] = client
|
||||
} else if client.HandshakeComplete() {
|
||||
isReused = true
|
||||
}
|
||||
client.useCount.Add(1)
|
||||
return client, isReused, nil
|
||||
}
|
||||
|
||||
func (r *RoundTripper) removeClient(hostname string) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if r.clients == nil {
|
||||
return
|
||||
}
|
||||
delete(r.clients, hostname)
|
||||
}
|
||||
|
||||
// Close closes the QUIC connections that this RoundTripper has used.
|
||||
// It also closes the underlying UDPConn if it is not nil.
|
||||
func (r *RoundTripper) Close() error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for _, client := range r.clients {
|
||||
if err := client.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.clients = nil
|
||||
if r.transport != nil {
|
||||
if err := r.transport.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.transport.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.transport = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeRequestBody(req *http.Request) {
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func validMethod(method string) bool {
|
||||
/*
|
||||
Method = "OPTIONS" ; Section 9.2
|
||||
| "GET" ; Section 9.3
|
||||
| "HEAD" ; Section 9.4
|
||||
| "POST" ; Section 9.5
|
||||
| "PUT" ; Section 9.6
|
||||
| "DELETE" ; Section 9.7
|
||||
| "TRACE" ; Section 9.8
|
||||
| "CONNECT" ; Section 9.9
|
||||
| extension-method
|
||||
extension-method = token
|
||||
token = 1*<any CHAR except CTLs or separators>
|
||||
*/
|
||||
return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
|
||||
}
|
||||
|
||||
// copied from net/http/http.go
|
||||
func isNotToken(r rune) bool {
|
||||
return !httpguts.IsTokenRune(r)
|
||||
}
|
||||
|
||||
// makeDialer makes a QUIC dialer using r.udpConn.
|
||||
func (r *RoundTripper) makeDialer() func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
return func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.transport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RoundTripper) CloseIdleConnections() {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for hostname, client := range r.clients {
|
||||
if client.useCount.Load() == 0 {
|
||||
client.Close()
|
||||
delete(r.clients, hostname)
|
||||
}
|
||||
}
|
||||
}
|
370
http3/roundtrip_test.go
Normal file
370
http3/roundtrip_test.go
Normal file
|
@ -0,0 +1,370 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/qerr"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type mockBody struct {
|
||||
reader bytes.Reader
|
||||
readErr error
|
||||
closeErr error
|
||||
closed bool
|
||||
}
|
||||
|
||||
// make sure the mockBody can be used as a http.Request.Body
|
||||
var _ io.ReadCloser = &mockBody{}
|
||||
|
||||
func (m *mockBody) Read(p []byte) (int, error) {
|
||||
if m.readErr != nil {
|
||||
return 0, m.readErr
|
||||
}
|
||||
return m.reader.Read(p)
|
||||
}
|
||||
|
||||
func (m *mockBody) SetData(data []byte) {
|
||||
m.reader = *bytes.NewReader(data)
|
||||
}
|
||||
|
||||
func (m *mockBody) Close() error {
|
||||
m.closed = true
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
var _ = Describe("RoundTripper", func() {
|
||||
var (
|
||||
rt *RoundTripper
|
||||
req *http.Request
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
rt = &RoundTripper{}
|
||||
var err error
|
||||
req, err = http.NewRequest("GET", "https://www.example.org/file1.html", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("dialing hosts", func() {
|
||||
It("creates new clients", func() {
|
||||
testErr := errors.New("test err")
|
||||
req, err := http.NewRequest("GET", "https://quic.clemente.io/foobar.html", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).Return(nil, testErr)
|
||||
return cl, nil
|
||||
}
|
||||
_, err = rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError(testErr))
|
||||
})
|
||||
|
||||
It("uses the quic.Config, if provided", func() {
|
||||
config := &quic.Config{HandshakeIdleTimeout: time.Millisecond}
|
||||
var receivedConfig *quic.Config
|
||||
rt.Dial = func(_ context.Context, _ string, _ *tls.Config, config *quic.Config) (quic.EarlyConnection, error) {
|
||||
receivedConfig = config
|
||||
return nil, errors.New("handshake error")
|
||||
}
|
||||
rt.QuicConfig = config
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("handshake error"))
|
||||
Expect(receivedConfig.HandshakeIdleTimeout).To(Equal(config.HandshakeIdleTimeout))
|
||||
})
|
||||
|
||||
It("uses the custom dialer, if provided", func() {
|
||||
var dialed bool
|
||||
dialer := func(_ context.Context, _ string, tlsCfgP *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
dialed = true
|
||||
return nil, errors.New("handshake error")
|
||||
}
|
||||
rt.Dial = dialer
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("handshake error"))
|
||||
Expect(dialed).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("reusing clients", func() {
|
||||
var req1, req2 *http.Request
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
req1, err = http.NewRequest("GET", "https://quic.clemente.io/file1.html", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
req2, err = http.NewRequest("GET", "https://quic.clemente.io/file2.html", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(req1.URL).ToNot(Equal(req2.URL))
|
||||
})
|
||||
|
||||
It("reuses existing clients", func() {
|
||||
var count int
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
count++
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(req *http.Request, _ RoundTripOpt) (*http.Response, error) {
|
||||
return &http.Response{Request: req}, nil
|
||||
}).Times(2)
|
||||
cl.EXPECT().HandshakeComplete().Return(true)
|
||||
return cl, nil
|
||||
}
|
||||
rsp1, err := rt.RoundTrip(req1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rsp1.Request.URL).To(Equal(req1.URL))
|
||||
rsp2, err := rt.RoundTrip(req2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rsp2.Request.URL).To(Equal(req2.URL))
|
||||
Expect(count).To(Equal(1))
|
||||
})
|
||||
|
||||
It("immediately removes a clients when a request errored", func() {
|
||||
testErr := errors.New("test err")
|
||||
|
||||
var count int
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
count++
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).Return(nil, testErr)
|
||||
return cl, nil
|
||||
}
|
||||
_, err := rt.RoundTrip(req1)
|
||||
Expect(err).To(MatchError(testErr))
|
||||
_, err = rt.RoundTrip(req2)
|
||||
Expect(err).To(MatchError(testErr))
|
||||
Expect(count).To(Equal(2))
|
||||
})
|
||||
|
||||
It("recreates a client when a request times out", func() {
|
||||
var reqCount int
|
||||
cl1 := NewMockRoundTripCloser(mockCtrl)
|
||||
cl1.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(req *http.Request, _ RoundTripOpt) (*http.Response, error) {
|
||||
reqCount++
|
||||
if reqCount == 1 { // the first request is successful...
|
||||
Expect(req.URL).To(Equal(req1.URL))
|
||||
return &http.Response{Request: req}, nil
|
||||
}
|
||||
// ... after that, the connection timed out in the background
|
||||
Expect(req.URL).To(Equal(req2.URL))
|
||||
return nil, &qerr.IdleTimeoutError{}
|
||||
}).Times(2)
|
||||
cl1.EXPECT().HandshakeComplete().Return(true)
|
||||
cl2 := NewMockRoundTripCloser(mockCtrl)
|
||||
cl2.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(req *http.Request, _ RoundTripOpt) (*http.Response, error) {
|
||||
return &http.Response{Request: req}, nil
|
||||
})
|
||||
|
||||
var count int
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
count++
|
||||
if count == 1 {
|
||||
return cl1, nil
|
||||
}
|
||||
return cl2, nil
|
||||
}
|
||||
rsp1, err := rt.RoundTrip(req1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rsp1.Request.RemoteAddr).To(Equal(req1.RemoteAddr))
|
||||
rsp2, err := rt.RoundTrip(req2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rsp2.Request.RemoteAddr).To(Equal(req2.RemoteAddr))
|
||||
})
|
||||
|
||||
It("only issues a request once, even if a timeout error occurs", func() {
|
||||
var count int
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
count++
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).Return(nil, &qerr.IdleTimeoutError{})
|
||||
return cl, nil
|
||||
}
|
||||
_, err := rt.RoundTrip(req1)
|
||||
Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
|
||||
Expect(count).To(Equal(1))
|
||||
})
|
||||
|
||||
It("handles a burst of requests", func() {
|
||||
wait := make(chan struct{})
|
||||
reqs := make(chan struct{}, 2)
|
||||
var count int
|
||||
rt.newClient = func(string, *tls.Config, *roundTripperOpts, *quic.Config, dialFunc) (roundTripCloser, error) {
|
||||
count++
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(req *http.Request, _ RoundTripOpt) (*http.Response, error) {
|
||||
reqs <- struct{}{}
|
||||
<-wait
|
||||
return nil, &qerr.IdleTimeoutError{}
|
||||
}).Times(2)
|
||||
cl.EXPECT().HandshakeComplete()
|
||||
return cl, nil
|
||||
}
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer func() { done <- struct{}{} }()
|
||||
_, err := rt.RoundTrip(req1)
|
||||
Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
|
||||
}()
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
defer func() { done <- struct{}{} }()
|
||||
_, err := rt.RoundTrip(req2)
|
||||
Expect(err).To(MatchError(&qerr.IdleTimeoutError{}))
|
||||
}()
|
||||
// wait for both requests to be issued
|
||||
Eventually(reqs).Should(Receive())
|
||||
Eventually(reqs).Should(Receive())
|
||||
close(wait) // now return the requests
|
||||
Eventually(done).Should(Receive())
|
||||
Eventually(done).Should(Receive())
|
||||
Expect(count).To(Equal(1))
|
||||
})
|
||||
|
||||
It("doesn't create new clients if RoundTripOpt.OnlyCachedConn is set", func() {
|
||||
req, err := http.NewRequest("GET", "https://quic.clemente.io/foobar.html", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = rt.RoundTripOpt(req, RoundTripOpt{OnlyCachedConn: true})
|
||||
Expect(err).To(MatchError(ErrNoCachedConn))
|
||||
})
|
||||
})
|
||||
|
||||
Context("validating request", func() {
|
||||
It("rejects plain HTTP requests", func() {
|
||||
req, err := http.NewRequest("GET", "http://www.example.org/", nil)
|
||||
req.Body = &mockBody{}
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: unsupported protocol scheme: http"))
|
||||
Expect(req.Body.(*mockBody).closed).To(BeTrue())
|
||||
})
|
||||
|
||||
It("rejects requests without a URL", func() {
|
||||
req.URL = nil
|
||||
req.Body = &mockBody{}
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: nil Request.URL"))
|
||||
Expect(req.Body.(*mockBody).closed).To(BeTrue())
|
||||
})
|
||||
|
||||
It("rejects request without a URL Host", func() {
|
||||
req.URL.Host = ""
|
||||
req.Body = &mockBody{}
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: no Host in request URL"))
|
||||
Expect(req.Body.(*mockBody).closed).To(BeTrue())
|
||||
})
|
||||
|
||||
It("doesn't try to close the body if the request doesn't have one", func() {
|
||||
req.URL = nil
|
||||
Expect(req.Body).To(BeNil())
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: nil Request.URL"))
|
||||
})
|
||||
|
||||
It("rejects requests without a header", func() {
|
||||
req.Header = nil
|
||||
req.Body = &mockBody{}
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: nil Request.Header"))
|
||||
Expect(req.Body.(*mockBody).closed).To(BeTrue())
|
||||
})
|
||||
|
||||
It("rejects requests with invalid header name fields", func() {
|
||||
req.Header.Add("foobär", "value")
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: invalid http header field name \"foobär\""))
|
||||
})
|
||||
|
||||
It("rejects requests with invalid header name values", func() {
|
||||
req.Header.Add("foo", string([]byte{0x7}))
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err.Error()).To(ContainSubstring("http3: invalid http header field value"))
|
||||
})
|
||||
|
||||
It("rejects requests with an invalid request method", func() {
|
||||
req.Method = "foobär"
|
||||
req.Body = &mockBody{}
|
||||
_, err := rt.RoundTrip(req)
|
||||
Expect(err).To(MatchError("http3: invalid method \"foobär\""))
|
||||
Expect(req.Body.(*mockBody).closed).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("closing", func() {
|
||||
It("closes", func() {
|
||||
rt.clients = make(map[string]*roundTripCloserWithCount)
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().Close()
|
||||
rt.clients["foo.bar"] = &roundTripCloserWithCount{cl, atomic.Int64{}}
|
||||
err := rt.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(rt.clients)).To(BeZero())
|
||||
})
|
||||
|
||||
It("closes a RoundTripper that has never been used", func() {
|
||||
Expect(len(rt.clients)).To(BeZero())
|
||||
err := rt.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(rt.clients)).To(BeZero())
|
||||
})
|
||||
|
||||
It("closes idle connections", func() {
|
||||
Expect(len(rt.clients)).To(Equal(0))
|
||||
req1, err := http.NewRequest("GET", "https://site1.com", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
req2, err := http.NewRequest("GET", "https://site2.com", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(req1.Host).ToNot(Equal(req2.Host))
|
||||
ctx1, cancel1 := context.WithCancel(context.Background())
|
||||
ctx2, cancel2 := context.WithCancel(context.Background())
|
||||
req1 = req1.WithContext(ctx1)
|
||||
req2 = req2.WithContext(ctx2)
|
||||
roundTripCalled := make(chan struct{})
|
||||
reqFinished := make(chan struct{})
|
||||
rt.newClient = func(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) {
|
||||
cl := NewMockRoundTripCloser(mockCtrl)
|
||||
cl.EXPECT().Close()
|
||||
cl.EXPECT().RoundTripOpt(gomock.Any(), gomock.Any()).DoAndReturn(func(r *http.Request, _ RoundTripOpt) (*http.Response, error) {
|
||||
roundTripCalled <- struct{}{}
|
||||
<-r.Context().Done()
|
||||
return nil, nil
|
||||
})
|
||||
return cl, nil
|
||||
}
|
||||
go func() {
|
||||
rt.RoundTrip(req1)
|
||||
reqFinished <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
rt.RoundTrip(req2)
|
||||
reqFinished <- struct{}{}
|
||||
}()
|
||||
<-roundTripCalled
|
||||
<-roundTripCalled
|
||||
// Both two requests are started.
|
||||
Expect(len(rt.clients)).To(Equal(2))
|
||||
cancel1()
|
||||
<-reqFinished
|
||||
// req1 is finished
|
||||
rt.CloseIdleConnections()
|
||||
Expect(len(rt.clients)).To(Equal(1))
|
||||
cancel2()
|
||||
<-reqFinished
|
||||
// all requests are finished
|
||||
rt.CloseIdleConnections()
|
||||
Expect(len(rt.clients)).To(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
785
http3/server.go
Normal file
785
http3/server.go
Normal file
|
@ -0,0 +1,785 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
ctls "crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
"github.com/quic-go/quic-go/internal/utils"
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
|
||||
"github.com/quic-go/qpack"
|
||||
)
|
||||
|
||||
// allows mocking of quic.Listen and quic.ListenAddr
|
||||
var (
|
||||
quicListen = func(conn net.PacketConn, tlsConf *tls.Config, config *quic.Config) (QUICEarlyListener, error) {
|
||||
return quic.ListenEarly(conn, tlsConf, config)
|
||||
}
|
||||
quicListenAddr = func(addr string, tlsConf *tls.Config, config *quic.Config) (QUICEarlyListener, error) {
|
||||
return quic.ListenAddrEarly(addr, tlsConf, config)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// NextProtoH3Draft29 is the ALPN protocol negotiated during the TLS handshake, for QUIC draft 29.
|
||||
NextProtoH3Draft29 = "h3-29"
|
||||
// NextProtoH3 is the ALPN protocol negotiated during the TLS handshake, for QUIC v1 and v2.
|
||||
NextProtoH3 = "h3"
|
||||
)
|
||||
|
||||
// StreamType is the stream type of a unidirectional stream.
|
||||
type StreamType uint64
|
||||
|
||||
const (
|
||||
streamTypeControlStream = 0
|
||||
streamTypePushStream = 1
|
||||
streamTypeQPACKEncoderStream = 2
|
||||
streamTypeQPACKDecoderStream = 3
|
||||
)
|
||||
|
||||
// A QUICEarlyListener listens for incoming QUIC connections.
|
||||
type QUICEarlyListener interface {
|
||||
Accept(context.Context) (quic.EarlyConnection, error)
|
||||
Addr() net.Addr
|
||||
io.Closer
|
||||
}
|
||||
|
||||
var _ QUICEarlyListener = &quic.EarlyListener{}
|
||||
|
||||
func versionToALPN(v protocol.VersionNumber) string {
|
||||
//nolint:exhaustive // These are all the versions we care about.
|
||||
switch v {
|
||||
case protocol.Version1, protocol.Version2:
|
||||
return NextProtoH3
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureTLSConfig creates a new tls.Config which can be used
|
||||
// to create a quic.Listener meant for serving http3. The created
|
||||
// tls.Config adds the functionality of detecting the used QUIC version
|
||||
// in order to set the correct ALPN value for the http3 connection.
|
||||
func ConfigureTLSConfig(tlsConf *tls.Config) *tls.Config {
|
||||
// The tls.Config used to setup the quic.Listener needs to have the GetConfigForClient callback set.
|
||||
// That way, we can get the QUIC version and set the correct ALPN value.
|
||||
return &tls.Config{
|
||||
GetConfigForClient: func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
// determine the ALPN from the QUIC version used
|
||||
proto := NextProtoH3
|
||||
val := ch.Context().Value(quic.QUICVersionContextKey)
|
||||
if v, ok := val.(quic.VersionNumber); ok {
|
||||
proto = versionToALPN(v)
|
||||
}
|
||||
config := tlsConf
|
||||
if tlsConf.GetConfigForClient != nil {
|
||||
getConfigForClient := tlsConf.GetConfigForClient
|
||||
var err error
|
||||
conf, err := getConfigForClient(ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conf != nil {
|
||||
config = conf
|
||||
}
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
config = config.Clone()
|
||||
config.NextProtos = []string{proto}
|
||||
return config, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string { return "quic-go/http3 context value " + k.name }
|
||||
|
||||
// ServerContextKey is a context key. It can be used in HTTP
|
||||
// handlers with Context.Value to access the server that
|
||||
// started the handler. The associated value will be of
|
||||
// type *http3.Server.
|
||||
var ServerContextKey = &contextKey{"http3-server"}
|
||||
|
||||
type requestError struct {
|
||||
err error
|
||||
streamErr ErrCode
|
||||
connErr ErrCode
|
||||
}
|
||||
|
||||
func newStreamError(code ErrCode, err error) requestError {
|
||||
return requestError{err: err, streamErr: code}
|
||||
}
|
||||
|
||||
func newConnError(code ErrCode, err error) requestError {
|
||||
return requestError{err: err, connErr: code}
|
||||
}
|
||||
|
||||
// listenerInfo contains info about specific listener added with addListener
|
||||
type listenerInfo struct {
|
||||
port int // 0 means that no info about port is available
|
||||
}
|
||||
|
||||
// Server is a HTTP/3 server.
|
||||
type Server struct {
|
||||
// Addr optionally specifies the UDP address for the server to listen on,
|
||||
// in the form "host:port".
|
||||
//
|
||||
// When used by ListenAndServe and ListenAndServeTLS methods, if empty,
|
||||
// ":https" (port 443) is used. See net.Dial for details of the address
|
||||
// format.
|
||||
//
|
||||
// Otherwise, if Port is not set and underlying QUIC listeners do not
|
||||
// have valid port numbers, the port part is used in Alt-Svc headers set
|
||||
// with SetQuicHeaders.
|
||||
Addr string
|
||||
|
||||
// Port is used in Alt-Svc response headers set with SetQuicHeaders. If
|
||||
// needed Port can be manually set when the Server is created.
|
||||
//
|
||||
// This is useful when a Layer 4 firewall is redirecting UDP traffic and
|
||||
// clients must use a port different from the port the Server is
|
||||
// listening on.
|
||||
Port int
|
||||
|
||||
// TLSConfig provides a TLS configuration for use by server. It must be
|
||||
// set for ListenAndServe and Serve methods.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// QuicConfig provides the parameters for QUIC connection created with
|
||||
// Serve. If nil, it uses reasonable default values.
|
||||
//
|
||||
// Configured versions are also used in Alt-Svc response header set with
|
||||
// SetQuicHeaders.
|
||||
QuicConfig *quic.Config
|
||||
|
||||
// Handler is the HTTP request handler to use. If not set, defaults to
|
||||
// http.NotFound.
|
||||
Handler http.Handler
|
||||
|
||||
// EnableDatagrams enables support for HTTP/3 datagrams.
|
||||
// If set to true, QuicConfig.EnableDatagram will be set.
|
||||
// See https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-07.
|
||||
EnableDatagrams bool
|
||||
|
||||
// MaxHeaderBytes controls the maximum number of bytes the server will
|
||||
// read parsing the request HEADERS frame. It does not limit the size of
|
||||
// the request body. If zero or negative, http.DefaultMaxHeaderBytes is
|
||||
// used.
|
||||
MaxHeaderBytes int
|
||||
|
||||
// AdditionalSettings specifies additional HTTP/3 settings.
|
||||
// It is invalid to specify any settings defined by the HTTP/3 draft and the datagram draft.
|
||||
AdditionalSettings map[uint64]uint64
|
||||
|
||||
// StreamHijacker, when set, is called for the first unknown frame parsed on a bidirectional stream.
|
||||
// It is called right after parsing the frame type.
|
||||
// If parsing the frame type fails, the error is passed to the callback.
|
||||
// In that case, the frame type will not be set.
|
||||
// Callers can either ignore the frame and return control of the stream back to HTTP/3
|
||||
// (by returning hijacked false).
|
||||
// Alternatively, callers can take over the QUIC stream (by returning hijacked true).
|
||||
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
|
||||
|
||||
// UniStreamHijacker, when set, is called for unknown unidirectional stream of unknown stream type.
|
||||
// If parsing the stream type fails, the error is passed to the callback.
|
||||
// In that case, the stream type will not be set.
|
||||
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
|
||||
|
||||
mutex sync.RWMutex
|
||||
listeners map[*QUICEarlyListener]listenerInfo
|
||||
|
||||
closed bool
|
||||
|
||||
altSvcHeader string
|
||||
|
||||
logger utils.Logger
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the UDP address s.Addr and calls s.Handler to handle HTTP/3 requests on incoming connections.
|
||||
//
|
||||
// If s.Addr is blank, ":https" is used.
|
||||
func (s *Server) ListenAndServe() error {
|
||||
return s.serveConn(s.TLSConfig, nil)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS listens on the UDP address s.Addr and calls s.Handler to handle HTTP/3 requests on incoming connections.
|
||||
//
|
||||
// If s.Addr is blank, ":https" is used.
|
||||
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
|
||||
var err error
|
||||
certs := make([]tls.Certificate, 1)
|
||||
certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We currently only use the cert-related stuff from tls.Config,
|
||||
// so we don't need to make a full copy.
|
||||
config := &tls.Config{
|
||||
Certificates: certs,
|
||||
}
|
||||
return s.serveConn(config, nil)
|
||||
}
|
||||
|
||||
// Serve an existing UDP connection.
|
||||
// It is possible to reuse the same connection for outgoing connections.
|
||||
// Closing the server does not close the connection.
|
||||
func (s *Server) Serve(conn net.PacketConn) error {
|
||||
return s.serveConn(s.TLSConfig, conn)
|
||||
}
|
||||
|
||||
// ServeQUICConn serves a single QUIC connection.
|
||||
func (s *Server) ServeQUICConn(conn quic.Connection) error {
|
||||
s.mutex.Lock()
|
||||
if s.logger == nil {
|
||||
s.logger = utils.DefaultLogger.WithPrefix("server")
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
|
||||
return s.handleConn(conn)
|
||||
}
|
||||
|
||||
// ServeListener serves an existing QUIC listener.
|
||||
// Make sure you use http3.ConfigureTLSConfig to configure a tls.Config
|
||||
// and use it to construct a http3-friendly QUIC listener.
|
||||
// Closing the server does close the listener.
|
||||
// ServeListener always returns a non-nil error. After Shutdown or Close, the returned error is http.ErrServerClosed.
|
||||
func (s *Server) ServeListener(ln QUICEarlyListener) error {
|
||||
if err := s.addListener(&ln); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.removeListener(&ln)
|
||||
for {
|
||||
conn, err := ln.Accept(context.Background())
|
||||
if err == quic.ErrServerClosed {
|
||||
return http.ErrServerClosed
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
if err := s.handleConn(conn); err != nil {
|
||||
s.logger.Debugf(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var errServerWithoutTLSConfig = errors.New("use of http3.Server without TLSConfig")
|
||||
|
||||
func (s *Server) serveConn(tlsConf *tls.Config, conn net.PacketConn) error {
|
||||
if tlsConf == nil {
|
||||
return errServerWithoutTLSConfig
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
closed := s.closed
|
||||
s.mutex.Unlock()
|
||||
if closed {
|
||||
return http.ErrServerClosed
|
||||
}
|
||||
|
||||
baseConf := ConfigureTLSConfig(tlsConf)
|
||||
quicConf := s.QuicConfig
|
||||
if quicConf == nil {
|
||||
quicConf = &quic.Config{Allow0RTT: true}
|
||||
} else {
|
||||
quicConf = s.QuicConfig.Clone()
|
||||
}
|
||||
if s.EnableDatagrams {
|
||||
quicConf.EnableDatagrams = true
|
||||
}
|
||||
|
||||
var ln QUICEarlyListener
|
||||
var err error
|
||||
if conn == nil {
|
||||
addr := s.Addr
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
ln, err = quicListenAddr(addr, baseConf, quicConf)
|
||||
} else {
|
||||
ln, err = quicListen(conn, baseConf, quicConf)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ServeListener(ln)
|
||||
}
|
||||
|
||||
func extractPort(addr string) (int, error) {
|
||||
_, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
portInt, err := net.LookupPort("tcp", portStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return portInt, nil
|
||||
}
|
||||
|
||||
func (s *Server) generateAltSvcHeader() {
|
||||
if len(s.listeners) == 0 {
|
||||
// Don't announce any ports since no one is listening for connections
|
||||
s.altSvcHeader = ""
|
||||
return
|
||||
}
|
||||
|
||||
// This code assumes that we will use protocol.SupportedVersions if no quic.Config is passed.
|
||||
supportedVersions := protocol.SupportedVersions
|
||||
if s.QuicConfig != nil && len(s.QuicConfig.Versions) > 0 {
|
||||
supportedVersions = s.QuicConfig.Versions
|
||||
}
|
||||
|
||||
// keep track of which have been seen so we don't yield duplicate values
|
||||
seen := make(map[string]struct{}, len(supportedVersions))
|
||||
var versionStrings []string
|
||||
for _, version := range supportedVersions {
|
||||
if v := versionToALPN(version); len(v) > 0 {
|
||||
if _, ok := seen[v]; !ok {
|
||||
versionStrings = append(versionStrings, v)
|
||||
seen[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var altSvc []string
|
||||
addPort := func(port int) {
|
||||
for _, v := range versionStrings {
|
||||
altSvc = append(altSvc, fmt.Sprintf(`%s=":%d"; ma=2592000`, v, port))
|
||||
}
|
||||
}
|
||||
|
||||
if s.Port != 0 {
|
||||
// if Port is specified, we must use it instead of the
|
||||
// listener addresses since there's a reason it's specified.
|
||||
addPort(s.Port)
|
||||
} else {
|
||||
// if we have some listeners assigned, try to find ports
|
||||
// which we can announce, otherwise nothing should be announced
|
||||
validPortsFound := false
|
||||
for _, info := range s.listeners {
|
||||
if info.port != 0 {
|
||||
addPort(info.port)
|
||||
validPortsFound = true
|
||||
}
|
||||
}
|
||||
if !validPortsFound {
|
||||
if port, err := extractPort(s.Addr); err == nil {
|
||||
addPort(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.altSvcHeader = strings.Join(altSvc, ",")
|
||||
}
|
||||
|
||||
// We store a pointer to interface in the map set. This is safe because we only
|
||||
// call trackListener via Serve and can track+defer untrack the same pointer to
|
||||
// local variable there. We never need to compare a Listener from another caller.
|
||||
func (s *Server) addListener(l *QUICEarlyListener) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return http.ErrServerClosed
|
||||
}
|
||||
if s.logger == nil {
|
||||
s.logger = utils.DefaultLogger.WithPrefix("server")
|
||||
}
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[*QUICEarlyListener]listenerInfo)
|
||||
}
|
||||
|
||||
if port, err := extractPort((*l).Addr().String()); err == nil {
|
||||
s.listeners[l] = listenerInfo{port}
|
||||
} else {
|
||||
s.logger.Errorf("Unable to extract port from listener %+v, will not be announced using SetQuicHeaders: %s", err)
|
||||
s.listeners[l] = listenerInfo{}
|
||||
}
|
||||
s.generateAltSvcHeader()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) removeListener(l *QUICEarlyListener) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
delete(s.listeners, l)
|
||||
s.generateAltSvcHeader()
|
||||
}
|
||||
|
||||
func (s *Server) handleConn(conn quic.Connection) error {
|
||||
decoder := qpack.NewDecoder(nil)
|
||||
|
||||
// send a SETTINGS frame
|
||||
str, err := conn.OpenUniStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening the control stream failed: %w", err)
|
||||
}
|
||||
b := make([]byte, 0, 64)
|
||||
b = quicvarint.Append(b, streamTypeControlStream) // stream type
|
||||
b = (&settingsFrame{Datagram: s.EnableDatagrams, Other: s.AdditionalSettings}).Append(b)
|
||||
str.Write(b)
|
||||
|
||||
go s.handleUnidirectionalStreams(conn)
|
||||
|
||||
// Process all requests immediately.
|
||||
// It's the client's responsibility to decide which requests are eligible for 0-RTT.
|
||||
for {
|
||||
str, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
var appErr *quic.ApplicationError
|
||||
if errors.As(err, &appErr) && appErr.ErrorCode == quic.ApplicationErrorCode(ErrCodeNoError) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("accepting stream failed: %w", err)
|
||||
}
|
||||
go func() {
|
||||
rerr := s.handleRequest(conn, str, decoder, func() {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "")
|
||||
})
|
||||
if rerr.err == errHijacked {
|
||||
return
|
||||
}
|
||||
if rerr.err != nil || rerr.streamErr != 0 || rerr.connErr != 0 {
|
||||
s.logger.Debugf("Handling request failed: %s", err)
|
||||
if rerr.streamErr != 0 {
|
||||
str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
|
||||
}
|
||||
if rerr.connErr != 0 {
|
||||
var reason string
|
||||
if rerr.err != nil {
|
||||
reason = rerr.err.Error()
|
||||
}
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
|
||||
}
|
||||
return
|
||||
}
|
||||
str.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleUnidirectionalStreams(conn quic.Connection) {
|
||||
for {
|
||||
str, err := conn.AcceptUniStream(context.Background())
|
||||
if err != nil {
|
||||
s.logger.Debugf("accepting unidirectional stream failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func(str quic.ReceiveStream) {
|
||||
streamType, err := quicvarint.Read(quicvarint.NewReader(str))
|
||||
if err != nil {
|
||||
if s.UniStreamHijacker != nil && s.UniStreamHijacker(StreamType(streamType), conn, str, err) {
|
||||
return
|
||||
}
|
||||
s.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err)
|
||||
return
|
||||
}
|
||||
// We're only interested in the control stream here.
|
||||
switch streamType {
|
||||
case streamTypeControlStream:
|
||||
case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream:
|
||||
// Our QPACK implementation doesn't use the dynamic table yet.
|
||||
// TODO: check that only one stream of each type is opened.
|
||||
return
|
||||
case streamTypePushStream: // only the server can push
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
|
||||
return
|
||||
default:
|
||||
if s.UniStreamHijacker != nil && s.UniStreamHijacker(StreamType(streamType), conn, str, nil) {
|
||||
return
|
||||
}
|
||||
str.CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError))
|
||||
return
|
||||
}
|
||||
f, err := parseNextFrame(str, nil)
|
||||
if err != nil {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
|
||||
return
|
||||
}
|
||||
sf, ok := f.(*settingsFrame)
|
||||
if !ok {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
|
||||
return
|
||||
}
|
||||
if !sf.Datagram {
|
||||
return
|
||||
}
|
||||
// If datagram support was enabled on our side as well as on the client side,
|
||||
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
|
||||
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
|
||||
if s.EnableDatagrams && !conn.ConnectionState().SupportsDatagrams {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
|
||||
}
|
||||
}(str)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) maxHeaderBytes() uint64 {
|
||||
if s.MaxHeaderBytes <= 0 {
|
||||
return http.DefaultMaxHeaderBytes
|
||||
}
|
||||
return uint64(s.MaxHeaderBytes)
|
||||
}
|
||||
|
||||
func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *qpack.Decoder, onFrameError func()) requestError {
|
||||
var ufh unknownFrameHandlerFunc
|
||||
if s.StreamHijacker != nil {
|
||||
ufh = func(ft FrameType, e error) (processed bool, err error) { return s.StreamHijacker(ft, conn, str, e) }
|
||||
}
|
||||
frame, err := parseNextFrame(str, ufh)
|
||||
if err != nil {
|
||||
if err == errHijacked {
|
||||
return requestError{err: errHijacked}
|
||||
}
|
||||
return newStreamError(ErrCodeRequestIncomplete, err)
|
||||
}
|
||||
hf, ok := frame.(*headersFrame)
|
||||
if !ok {
|
||||
return newConnError(ErrCodeFrameUnexpected, errors.New("expected first frame to be a HEADERS frame"))
|
||||
}
|
||||
if hf.Length > s.maxHeaderBytes() {
|
||||
return newStreamError(ErrCodeFrameError, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes()))
|
||||
}
|
||||
headerBlock := make([]byte, hf.Length)
|
||||
if _, err := io.ReadFull(str, headerBlock); err != nil {
|
||||
return newStreamError(ErrCodeRequestIncomplete, err)
|
||||
}
|
||||
hfs, err := decoder.DecodeFull(headerBlock)
|
||||
if err != nil {
|
||||
// TODO: use the right error code
|
||||
return newConnError(ErrCodeGeneralProtocolError, err)
|
||||
}
|
||||
req, err := requestFromHeaders(hfs)
|
||||
if err != nil {
|
||||
return newStreamError(ErrCodeMessageError, err)
|
||||
}
|
||||
|
||||
connState := conn.ConnectionState().TLS
|
||||
|
||||
// [UQUIC] copy utls.ConnectionState to crypto/tls.ConnectionState
|
||||
cryptoConnState := &ctls.ConnectionState{
|
||||
Version: connState.Version,
|
||||
HandshakeComplete: connState.HandshakeComplete,
|
||||
DidResume: connState.DidResume,
|
||||
CipherSuite: connState.CipherSuite,
|
||||
NegotiatedProtocol: connState.NegotiatedProtocol,
|
||||
NegotiatedProtocolIsMutual: connState.NegotiatedProtocolIsMutual,
|
||||
ServerName: connState.ServerName,
|
||||
PeerCertificates: connState.PeerCertificates,
|
||||
VerifiedChains: connState.VerifiedChains,
|
||||
SignedCertificateTimestamps: connState.SignedCertificateTimestamps,
|
||||
OCSPResponse: connState.OCSPResponse,
|
||||
TLSUnique: connState.TLSUnique,
|
||||
}
|
||||
req.TLS = cryptoConnState
|
||||
// [/UQUIC]
|
||||
|
||||
req.RemoteAddr = conn.RemoteAddr().String()
|
||||
|
||||
// Check that the client doesn't send more data in DATA frames than indicated by the Content-Length header (if set).
|
||||
// See section 4.1.2 of RFC 9114.
|
||||
var httpStr Stream
|
||||
if _, ok := req.Header["Content-Length"]; ok && req.ContentLength >= 0 {
|
||||
httpStr = newLengthLimitedStream(newStream(str, onFrameError), req.ContentLength)
|
||||
} else {
|
||||
httpStr = newStream(str, onFrameError)
|
||||
}
|
||||
body := newRequestBody(httpStr)
|
||||
req.Body = body
|
||||
|
||||
if s.logger.Debug() {
|
||||
s.logger.Infof("%s %s%s, on stream %d", req.Method, req.Host, req.RequestURI, str.StreamID())
|
||||
} else {
|
||||
s.logger.Infof("%s %s%s", req.Method, req.Host, req.RequestURI)
|
||||
}
|
||||
|
||||
ctx := str.Context()
|
||||
ctx = context.WithValue(ctx, ServerContextKey, s)
|
||||
ctx = context.WithValue(ctx, http.LocalAddrContextKey, conn.LocalAddr())
|
||||
req = req.WithContext(ctx)
|
||||
r := newResponseWriter(str, conn, s.logger)
|
||||
handler := s.Handler
|
||||
if handler == nil {
|
||||
handler = http.DefaultServeMux
|
||||
}
|
||||
|
||||
var panicked bool
|
||||
func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicked = true
|
||||
if p == http.ErrAbortHandler {
|
||||
return
|
||||
}
|
||||
// Copied from net/http/server.go
|
||||
const size = 64 << 10
|
||||
buf := make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
s.logger.Errorf("http: panic serving: %v\n%s", p, buf)
|
||||
}
|
||||
}()
|
||||
handler.ServeHTTP(r, req)
|
||||
}()
|
||||
|
||||
if body.wasStreamHijacked() {
|
||||
return requestError{err: errHijacked}
|
||||
}
|
||||
|
||||
// only write response when there is no panic
|
||||
if !panicked {
|
||||
r.WriteHeader(http.StatusOK)
|
||||
r.Flush()
|
||||
}
|
||||
// If the EOF was read by the handler, CancelRead() is a no-op.
|
||||
str.CancelRead(quic.StreamErrorCode(ErrCodeNoError))
|
||||
return requestError{}
|
||||
}
|
||||
|
||||
// Close the server immediately, aborting requests and sending CONNECTION_CLOSE frames to connected clients.
|
||||
// Close in combination with ListenAndServe() (instead of Serve()) may race if it is called before a UDP socket is established.
|
||||
func (s *Server) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.closed = true
|
||||
|
||||
var err error
|
||||
for ln := range s.listeners {
|
||||
if cerr := (*ln).Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseGracefully shuts down the server gracefully. The server sends a GOAWAY frame first, then waits for either timeout to trigger, or for all running requests to complete.
|
||||
// CloseGracefully in combination with ListenAndServe() (instead of Serve()) may race if it is called before a UDP socket is established.
|
||||
func (s *Server) CloseGracefully(timeout time.Duration) error {
|
||||
// TODO: implement
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrNoAltSvcPort is the error returned by SetQuicHeaders when no port was found
|
||||
// for Alt-Svc to announce. This can happen if listening on a PacketConn without a port
|
||||
// (UNIX socket, for example) and no port is specified in Server.Port or Server.Addr.
|
||||
var ErrNoAltSvcPort = errors.New("no port can be announced, specify it explicitly using Server.Port or Server.Addr")
|
||||
|
||||
// SetQuicHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
|
||||
// The values set by default advertise all of the ports the server is listening on, but can be
|
||||
// changed to a specific port by setting Server.Port before launching the serverr.
|
||||
// If no listener's Addr().String() returns an address with a valid port, Server.Addr will be used
|
||||
// to extract the port, if specified.
|
||||
// For example, a server launched using ListenAndServe on an address with port 443 would set:
|
||||
//
|
||||
// Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
func (s *Server) SetQuicHeaders(hdr http.Header) error {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
if s.altSvcHeader == "" {
|
||||
return ErrNoAltSvcPort
|
||||
}
|
||||
// use the map directly to avoid constant canonicalization
|
||||
// since the key is already canonicalized
|
||||
hdr["Alt-Svc"] = append(hdr["Alt-Svc"], s.altSvcHeader)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenAndServeQUIC listens on the UDP network address addr and calls the
|
||||
// handler for HTTP/3 requests on incoming connections. http.DefaultServeMux is
|
||||
// used when handler is nil.
|
||||
func ListenAndServeQUIC(addr, certFile, keyFile string, handler http.Handler) error {
|
||||
server := &Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
}
|
||||
return server.ListenAndServeTLS(certFile, keyFile)
|
||||
}
|
||||
|
||||
// ListenAndServe listens on the given network address for both, TLS and QUIC
|
||||
// connections in parallel. It returns if one of the two returns an error.
|
||||
// http.DefaultServeMux is used when handler is nil.
|
||||
// The correct Alt-Svc headers for QUIC are set.
|
||||
func ListenAndServe(addr, certFile, keyFile string, handler http.Handler) error {
|
||||
// Load certs
|
||||
var err error
|
||||
certs := make([]tls.Certificate, 1)
|
||||
certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We currently only use the cert-related stuff from tls.Config,
|
||||
// so we don't need to make a full copy.
|
||||
config := &tls.Config{
|
||||
Certificates: certs,
|
||||
}
|
||||
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
|
||||
// Open the listeners
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
if handler == nil {
|
||||
handler = http.DefaultServeMux
|
||||
}
|
||||
// Start the servers
|
||||
quicServer := &Server{
|
||||
TLSConfig: config,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
hErr := make(chan error)
|
||||
qErr := make(chan error)
|
||||
go func() {
|
||||
hErr <- http.ListenAndServeTLS(addr, certFile, keyFile, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
quicServer.SetQuicHeaders(w.Header())
|
||||
handler.ServeHTTP(w, r)
|
||||
}))
|
||||
}()
|
||||
go func() {
|
||||
qErr <- quicServer.Serve(udpConn)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-hErr:
|
||||
quicServer.Close()
|
||||
return err
|
||||
case err := <-qErr:
|
||||
// Cannot close the HTTP server or wait for requests to complete properly :/
|
||||
return err
|
||||
}
|
||||
}
|
1240
http3/server_test.go
Normal file
1240
http3/server_test.go
Normal file
File diff suppressed because it is too large
Load diff
192
http3/u_roundtrip.go
Normal file
192
http3/u_roundtrip.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package http3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
tls "github.com/refraction-networking/utls"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
type URoundTripper struct {
|
||||
*RoundTripper
|
||||
|
||||
quicSpec *quic.QUICSpec
|
||||
uTransportOverride *quic.UTransport
|
||||
}
|
||||
|
||||
func GetURoundTripper(r *RoundTripper, QUICSpec *quic.QUICSpec, uTransport *quic.UTransport) *URoundTripper {
|
||||
QUICSpec.UpdateConfig(r.QuicConfig)
|
||||
|
||||
return &URoundTripper{
|
||||
RoundTripper: r,
|
||||
quicSpec: QUICSpec,
|
||||
uTransportOverride: uTransport,
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTripOpt is like RoundTrip, but takes options.
|
||||
func (r *URoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
|
||||
if req.URL == nil {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: nil Request.URL")
|
||||
}
|
||||
if req.URL.Scheme != "https" {
|
||||
closeRequestBody(req)
|
||||
return nil, fmt.Errorf("http3: unsupported protocol scheme: %s", req.URL.Scheme)
|
||||
}
|
||||
if req.URL.Host == "" {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: no Host in request URL")
|
||||
}
|
||||
if req.Header == nil {
|
||||
closeRequestBody(req)
|
||||
return nil, errors.New("http3: nil Request.Header")
|
||||
}
|
||||
for k, vv := range req.Header {
|
||||
if !httpguts.ValidHeaderFieldName(k) {
|
||||
return nil, fmt.Errorf("http3: invalid http header field name %q", k)
|
||||
}
|
||||
for _, v := range vv {
|
||||
if !httpguts.ValidHeaderFieldValue(v) {
|
||||
return nil, fmt.Errorf("http3: invalid http header field value %q for key %v", v, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method != "" && !validMethod(req.Method) {
|
||||
closeRequestBody(req)
|
||||
return nil, fmt.Errorf("http3: invalid method %q", req.Method)
|
||||
}
|
||||
|
||||
hostname := authorityAddr("https", hostnameFromRequest(req))
|
||||
cl, isReused, err := r.getClient(hostname, opt.OnlyCachedConn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cl.useCount.Add(-1)
|
||||
rsp, err := cl.RoundTripOpt(req, opt)
|
||||
if err != nil {
|
||||
r.removeClient(hostname)
|
||||
if isReused {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
return r.RoundTripOpt(req, opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
// RoundTrip does a round trip.
|
||||
func (r *URoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return r.RoundTripOpt(req, RoundTripOpt{})
|
||||
}
|
||||
|
||||
func (r *URoundTripper) getClient(hostname string, onlyCached bool) (rtc *roundTripCloserWithCount, isReused bool, err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.clients == nil {
|
||||
r.clients = make(map[string]*roundTripCloserWithCount)
|
||||
}
|
||||
|
||||
client, ok := r.clients[hostname]
|
||||
if !ok {
|
||||
if onlyCached {
|
||||
return nil, false, ErrNoCachedConn
|
||||
}
|
||||
var err error
|
||||
newCl := newClient
|
||||
if r.newClient != nil {
|
||||
newCl = r.newClient
|
||||
}
|
||||
dial := r.Dial
|
||||
if dial == nil {
|
||||
if r.transport == nil && r.uTransportOverride == nil {
|
||||
udpConn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
r.uTransportOverride = &quic.UTransport{
|
||||
Transport: &quic.Transport{
|
||||
Conn: udpConn,
|
||||
},
|
||||
QUICSpec: r.quicSpec,
|
||||
}
|
||||
}
|
||||
dial = r.makeDialer()
|
||||
}
|
||||
c, err := newCl(
|
||||
hostname,
|
||||
r.TLSClientConfig,
|
||||
&roundTripperOpts{
|
||||
EnableDatagram: r.EnableDatagrams,
|
||||
DisableCompression: r.DisableCompression,
|
||||
MaxHeaderBytes: r.MaxResponseHeaderBytes,
|
||||
StreamHijacker: r.StreamHijacker,
|
||||
UniStreamHijacker: r.UniStreamHijacker,
|
||||
},
|
||||
r.QuicConfig,
|
||||
dial,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
client = &roundTripCloserWithCount{roundTripCloser: c}
|
||||
r.clients[hostname] = client
|
||||
} else if client.HandshakeComplete() {
|
||||
isReused = true
|
||||
}
|
||||
client.useCount.Add(1)
|
||||
return client, isReused, nil
|
||||
}
|
||||
|
||||
func (r *URoundTripper) Close() error {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for _, client := range r.clients {
|
||||
if err := client.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.clients = nil
|
||||
if r.transport != nil {
|
||||
if err := r.transport.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.transport.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.transport = nil
|
||||
}
|
||||
if r.uTransportOverride != nil {
|
||||
if err := r.uTransportOverride.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.uTransportOverride.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.uTransportOverride = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeDialer makes a QUIC dialer using r.udpConn.
|
||||
func (r *URoundTripper) makeDialer() func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
return func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.uTransportOverride != nil {
|
||||
return r.uTransportOverride.DialEarly(ctx, udpAddr, tlsCfg, cfg)
|
||||
} else if r.transport == nil {
|
||||
return nil, errors.New("http3: no QUIC transport available")
|
||||
}
|
||||
return r.transport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
|
||||
}
|
||||
}
|
1
integrationtests/gomodvendor/.gitignore
vendored
Normal file
1
integrationtests/gomodvendor/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
vendor/
|
8
integrationtests/gomodvendor/go.mod
Normal file
8
integrationtests/gomodvendor/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module test
|
||||
|
||||
go 1.16
|
||||
|
||||
// The version doesn't matter here, as we're replacing it with the currently checked out code anyway.
|
||||
require github.com/quic-go/quic-go v0.21.0
|
||||
|
||||
replace github.com/quic-go/quic-go => ../../
|
369
integrationtests/gomodvendor/go.sum
Normal file
369
integrationtests/gomodvendor/go.sum
Normal file
|
@ -0,0 +1,369 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
|
||||
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
|
||||
github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
|
||||
github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
|
||||
github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
|
||||
github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
|
||||
github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
||||
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
||||
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
|
||||
github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
|
||||
github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
|
||||
github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
|
||||
github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
|
||||
github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
|
||||
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.0 h1:NrCXmDl8BddZwO67vlvEpBTwT89bJfKYygxv4HQvuDk=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.0/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
9
integrationtests/gomodvendor/main.go
Normal file
9
integrationtests/gomodvendor/main.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import "github.com/quic-go/quic-go/http3"
|
||||
|
||||
// The contents of this script don't matter.
|
||||
// We just need to make sure that quic-go is imported.
|
||||
func main() {
|
||||
_ = http3.Server{}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue