├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── acceptance-tests ├── get_ice_servers_test.go ├── server_connect_test.go └── server_measurements_test.go ├── adapters ├── api │ └── driver.go ├── cloudflare │ └── driver.go ├── elixir │ └── driver.go ├── expressturn │ └── driver.go ├── google │ └── driver.go ├── metered │ └── driver.go ├── serverconnect │ └── driver.go ├── stunner │ └── driver.go ├── twilio │ └── driver.go ├── types.go ├── webrtcpeerconnect │ └── driver.go └── xirsys │ └── driver.go ├── assets └── ICEPerf_fulllogo_nobuffer.png ├── client ├── client.go ├── dataChannel.go └── ice-servers.go ├── cmd └── iceperf │ └── main.go ├── config-api.yaml.example ├── config.yaml.example ├── config └── config.go ├── go.mod ├── go.sum ├── iceperf.sh ├── specifications └── specifications.go ├── stats └── stats.go ├── util └── check.go └── version └── version.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build and Test App 5 | 6 | on: 7 | # push: 8 | # tags: 9 | # - "v*.*.*" 10 | release: 11 | types: [created] 12 | # pull_request: 13 | # branches: [ "main" ] 14 | 15 | env: 16 | REGISTRY_IMAGE: nimbleape/iceperf-agent 17 | 18 | jobs: 19 | test: 20 | name: Test 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.22.x' 29 | 30 | - name: Install dependencies 31 | run: go get ./cmd/iceperf 32 | 33 | - name: Test 34 | run: go test -v ./acceptance-tests 35 | env: 36 | ENVIRONMENT: TEST 37 | METERED_USERNAME: ${{ secrets.METERED_USERNAME }} 38 | METERED_PASSWORD: ${{ secrets.METERED_PASSWORD }} 39 | METERED_API_KEY: ${{ secrets.METERED_API_KEY }} 40 | METERED_REQUEST_URL: ${{ secrets.METERED_REQUEST_URL }} 41 | TWILIO_HTTP_USERNAME: ${{ secrets.TWILIO_HTTP_USERNAME }} 42 | TWILIO_HTTP_PASSWORD: ${{ secrets.TWILIO_HTTP_PASSWORD }} 43 | TWILIO_REQUEST_URL: ${{ secrets.TWILIO_REQUEST_URL }} 44 | TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} 45 | XIRSYS_HTTP_USERNAME: ${{ secrets.XIRSYS_HTTP_USERNAME }} 46 | XIRSYS_HTTP_PASSWORD: ${{ secrets.XIRSYS_HTTP_PASSWORD }} 47 | XIRSYS_REQUEST_URL: ${{ secrets.XIRSYS_REQUEST_URL }} 48 | build-docker: 49 | name: Build Docker 50 | needs: 51 | - test 52 | concurrency: 53 | group: ${{ github.workflow }}-${{ matrix.platform }}-${{ github.ref }} 54 | cancel-in-progress: true 55 | runs-on: ubuntu-latest 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | platform: 60 | - linux/amd64 61 | # - linux/arm/v6 62 | - linux/arm/v7 63 | - linux/arm64 64 | # - linux/riscv64 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Prepare 68 | run: | 69 | platform=${{ matrix.platform }} 70 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 71 | 72 | - name: Docker meta 73 | id: meta 74 | uses: docker/metadata-action@v5 75 | with: 76 | images: ${{ env.REGISTRY_IMAGE }} 77 | 78 | - name: Set up QEMU 79 | uses: docker/setup-qemu-action@v3 80 | 81 | - name: Set up Docker Buildx 82 | uses: docker/setup-buildx-action@v3 83 | 84 | - name: Login to Docker Hub 85 | uses: docker/login-action@v3 86 | with: 87 | username: ${{ vars.DOCKERHUB_USERNAME }} 88 | password: ${{ secrets.DOCKERHUB_TOKEN }} 89 | 90 | - name: Build and push by digest 91 | id: build 92 | uses: docker/build-push-action@v6 93 | with: 94 | platforms: ${{ matrix.platform }} 95 | labels: ${{ steps.meta.outputs.labels }} 96 | outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true 97 | 98 | - name: Export digest 99 | run: | 100 | mkdir -p /tmp/digests 101 | digest="${{ steps.build.outputs.digest }}" 102 | touch "/tmp/digests/${digest#sha256:}" 103 | 104 | - name: Upload digest 105 | uses: actions/upload-artifact@v4 106 | with: 107 | name: digests-${{ env.PLATFORM_PAIR }} 108 | path: /tmp/digests/* 109 | if-no-files-found: error 110 | retention-days: 1 111 | merge: 112 | runs-on: ubuntu-latest 113 | needs: 114 | - build-docker 115 | steps: 116 | - name: Download digests 117 | uses: actions/download-artifact@v4 118 | with: 119 | path: /tmp/digests 120 | pattern: digests-* 121 | merge-multiple: true 122 | 123 | - name: Set up Docker Buildx 124 | uses: docker/setup-buildx-action@v3 125 | 126 | - name: Docker meta 127 | id: meta 128 | uses: docker/metadata-action@v5 129 | with: 130 | images: ${{ env.REGISTRY_IMAGE }} 131 | 132 | - name: Login to Docker Hub 133 | uses: docker/login-action@v3 134 | with: 135 | username: ${{ vars.DOCKERHUB_USERNAME }} 136 | password: ${{ secrets.DOCKERHUB_TOKEN }} 137 | 138 | - name: Create manifest list and push 139 | working-directory: /tmp/digests 140 | run: | 141 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 142 | $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) 143 | 144 | - name: Inspect image 145 | run: | 146 | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} 147 | build: 148 | name: Build Binary 149 | needs: 150 | - test 151 | concurrency: 152 | group: ${{ github.workflow }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ github.ref }} 153 | cancel-in-progress: true 154 | runs-on: ubuntu-latest 155 | strategy: 156 | matrix: 157 | # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64 158 | goos: [linux, windows, darwin] 159 | goarch: [amd64, arm64, riscv64] 160 | exclude: 161 | - goarch: arm64 162 | goos: windows 163 | - goarch: riscv64 164 | goos: windows 165 | - goarch: riscv64 166 | goos: darwin 167 | steps: 168 | - uses: actions/checkout@v4 169 | 170 | # - name: Set up Go 171 | # uses: actions/setup-go@v4 172 | # with: 173 | # go-version: '1.22.x' 174 | 175 | # - name: Install dependencies 176 | # run: go get ./cmd/iceperf 177 | 178 | - uses: wangyoucao577/go-release-action@v1 179 | with: 180 | github_token: ${{ secrets.GITHUB_TOKEN }} 181 | goos: ${{ matrix.goos }} 182 | goarch: ${{ matrix.goarch }} 183 | project_path: "./cmd/iceperf" 184 | overwrite: true 185 | asset_name: iceperf-agent-${{ matrix.goos }}-${{ matrix.goarch }} 186 | 187 | # - name: Build 188 | # run: go build -o ${{ matrix.binary-name }} ./cmd/iceperf 189 | 190 | # - name: Release 191 | # uses: softprops/action-gh-release@v2 192 | # if: startsWith(github.ref, 'refs/tags/') 193 | # with: 194 | # files: ${{ matrix.binary-name }} 195 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | .DS_Store 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | 24 | # .env files 25 | .env* 26 | 27 | # Config files 28 | config.yaml 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | RUN CGO_ENABLED=0 ldflags="-s -w" go build -o /iceperf-agent cmd/iceperf/main.go 12 | 13 | FROM gcr.io/distroless/static 14 | 15 | WORKDIR / 16 | 17 | COPY --from=builder /iceperf-agent . 18 | 19 | ENTRYPOINT ["./iceperf-agent"] 20 | CMD ["-h"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nimble Ape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICEPerf 2 | ![ICEPerf logo](assets/ICEPerf_fulllogo_nobuffer.png) 3 | 4 | ICEPerf is an open source project that helps you test and compare the performance of TURN networks. See more info and the latest results on [ICEPerf.com](https://iceperf.com). 5 | 6 | 7 | # ICEPerf CLI APP 8 | Run ICE servers performance tests and report the results. 9 | 10 | ## Installation 11 | You can download the pre-built binary, the docker image or download the code and build it locally. 12 | 13 | ### Binary 14 | Download the binary to run it natively - You can find these in the [github releases](https://github.com/nimbleape/iceperf-agent/releases). 15 | 16 | ### Docker 17 | 18 | Run the docker image as a container. 19 | 20 | `docker run -d nimbleape/iceperf-agent --timer --api-key=your-api-key` 21 | 22 | The default CLI flags passed into the binary is `-h` outputting the help information. 23 | 24 | If you want to pass in a config file you would need to pass in the config file as a file mount and the cli flags; like so: 25 | 26 | `docker run -v $PWD/config.yaml:/config.yaml -d nimbleape/iceperf-agent --config config.yaml` 27 | 28 | ### Build locally 29 | To install the local project, clone the repo and run the following command from the root folder: 30 | 31 | ```zsh 32 | go install ./cmd/iceperf 33 | ``` 34 | 35 | ## Running the app 36 | To run the app from the terminal, do: 37 | 38 | ```zsh 39 | iceperf --config path/to/config.yaml 40 | ``` 41 | 42 | ```zsh 43 | iceperf --api-key="foo" 44 | ``` 45 | 46 | ### Commands 47 | None yet. 48 | 49 | ### Flags 50 | - `--config` or `-c` to specify the path for the config `.yaml` file 51 | - `-h` or `--help` for the help menu 52 | - `-v` or `--version` for the app version 53 | - `--api-uri` or `-a` to specify the API URI 54 | - `--api-key` or `-k` to specify the API Key 55 | - `--timer` or `-t` to enable Timer Mode (default: false) 56 | 57 | ### Config file 58 | A `.yaml` file to provide ICE server providers credentials and other settings. Examlpes of two config files can be found in the repo. Rename `config-api.yaml.exmaple` and `config.yaml.example` to remove the `.example` extension. 59 | 60 | `config-api.yaml` is a minimal config when talking to the ICEPerf api. 61 | `config.yaml` is a full example if not talking to the ICEPerf api. 62 | -------------------------------------------------------------------------------- /acceptance-tests/get_ice_servers_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/nimbleape/iceperf-agent/adapters/cloudflare" 9 | "github.com/nimbleape/iceperf-agent/adapters/expressturn" 10 | "github.com/nimbleape/iceperf-agent/adapters/metered" 11 | "github.com/nimbleape/iceperf-agent/adapters/twilio" 12 | "github.com/nimbleape/iceperf-agent/adapters/xirsys" 13 | "github.com/nimbleape/iceperf-agent/config" 14 | "github.com/nimbleape/iceperf-agent/specifications" 15 | ) 16 | 17 | func TestMeteredICEServers(t *testing.T) { 18 | err := loadEnv() 19 | assert.NoError(t, err) 20 | 21 | API_KEY := os.Getenv("METERED_API_KEY") 22 | REQUEST_URL := os.Getenv("METERED_REQUEST_URL") 23 | 24 | meteredConfig := config.ICEConfig{ 25 | RequestUrl: REQUEST_URL, 26 | ApiKey: API_KEY, 27 | StunEnabled: true, 28 | TurnEnabled: true, 29 | } 30 | 31 | md := metered.Driver{ 32 | Config: &meteredConfig, 33 | } 34 | 35 | specifications.GetIceServersSpecification(t, &md) 36 | } 37 | 38 | func TestTwilioICEServers(t *testing.T) { 39 | err := loadEnv() 40 | assert.NoError(t, err) 41 | 42 | HTTP_USERNAME := os.Getenv("TWILIO_HTTP_USERNAME") 43 | HTTP_PASSWORD := os.Getenv("TWILIO_HTTP_PASSWORD") 44 | REQUEST_URL := os.Getenv("TWILIO_REQUEST_URL") 45 | 46 | td := twilio.Driver{ 47 | Config: &config.ICEConfig{ 48 | RequestUrl: REQUEST_URL, 49 | HttpUsername: HTTP_USERNAME, 50 | HttpPassword: HTTP_PASSWORD, 51 | StunEnabled: true, 52 | TurnEnabled: true, 53 | }, 54 | } 55 | 56 | specifications.GetIceServersSpecification(t, &td) 57 | } 58 | 59 | func TestXirsysICEServers(t *testing.T) { 60 | err := loadEnv() 61 | assert.NoError(t, err) 62 | 63 | HTTP_USERNAME := os.Getenv("XIRSYS_HTTP_USERNAME") 64 | HTTP_PASSWORD := os.Getenv("XIRSYS_HTTP_PASSWORD") 65 | REQUEST_URL := os.Getenv("XIRSYS_REQUEST_URL") 66 | 67 | xd := xirsys.Driver{ 68 | Config: &config.ICEConfig{ 69 | RequestUrl: REQUEST_URL, 70 | HttpUsername: HTTP_USERNAME, 71 | HttpPassword: HTTP_PASSWORD, 72 | StunEnabled: true, 73 | TurnEnabled: true, 74 | }, 75 | } 76 | 77 | specifications.GetIceServersSpecification(t, &xd) 78 | } 79 | func TestCloudflareICEServers(t *testing.T) { 80 | err := loadEnv() 81 | assert.NoError(t, err) 82 | 83 | USERNAME := os.Getenv("CLOUDFLARE_USERNAME") 84 | PASSWORD := os.Getenv("CLOUDFLARE_PASSWORD") 85 | 86 | turnPorts := make(map[string][]int) 87 | turnPorts["udp"] = []int{3478, 53} 88 | turnPorts["tcp"] = []int{3478, 80} 89 | turnPorts["tls"] = []int{5349, 443} 90 | 91 | cd := cloudflare.Driver{ 92 | Config: &config.ICEConfig{ 93 | Username: USERNAME, 94 | Password: PASSWORD, 95 | StunHost: "stun.cloudflare.com", 96 | TurnHost: "turn.cloudflare.com", 97 | TurnPorts: turnPorts, 98 | StunEnabled: true, 99 | TurnEnabled: true, 100 | }, 101 | } 102 | 103 | specifications.GetIceServersSpecification(t, &cd) 104 | } 105 | 106 | func TestExpressturnICEServers(t *testing.T) { 107 | err := loadEnv() 108 | assert.NoError(t, err) 109 | 110 | USERNAME := os.Getenv("EXPRESSTURN_USERNAME") 111 | PASSWORD := os.Getenv("EXPRESSTURN_PASSWORD") 112 | 113 | turnPorts := make(map[string][]int) 114 | turnPorts["udp"] = []int{3478, 80} 115 | turnPorts["tcp"] = []int{3478, 443} 116 | turnPorts["tls"] = []int{5349, 443} 117 | 118 | ed := expressturn.Driver{ 119 | Config: &config.ICEConfig{ 120 | Username: USERNAME, 121 | Password: PASSWORD, 122 | StunHost: "relay1.expressturn.com", 123 | TurnHost: "relay1.expressturn.com", 124 | TurnPorts: turnPorts, 125 | StunEnabled: true, 126 | TurnEnabled: true, 127 | }, 128 | } 129 | 130 | specifications.GetIceServersSpecification(t, &ed) 131 | } 132 | -------------------------------------------------------------------------------- /acceptance-tests/server_connect_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_tests 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | "github.com/joho/godotenv" 12 | "github.com/nimbleape/iceperf-agent/adapters/serverconnect" 13 | "github.com/nimbleape/iceperf-agent/adapters/webrtcpeerconnect" 14 | "github.com/nimbleape/iceperf-agent/specifications" 15 | "github.com/pion/webrtc/v4" 16 | ) 17 | 18 | // TODO remove 19 | func loadEnv() error { 20 | ENVIRONMENT := os.Getenv("ENVIRONMENT") 21 | if ENVIRONMENT != "TEST" { 22 | err := godotenv.Load("../.env") 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | // FIXME this is actually just a "Connect to TURN provider" test 29 | func TestConnectToServer(t *testing.T) { 30 | // FIXME use new config 31 | err := loadEnv() 32 | assert.NoError(t, err) 33 | 34 | API_KEY := os.Getenv("METERED_API_KEY") 35 | driver := serverconnect.Driver{ 36 | Url: fmt.Sprintf("https://relayperf.metered.live/api/v1/turn/credentials?apiKey=%s", API_KEY), 37 | Client: &http.Client{ 38 | Timeout: 1 * time.Second, 39 | }, 40 | } 41 | 42 | specifications.ConnectToServerSpecification(t, driver) 43 | } 44 | 45 | func TestConnectToTURNServer(t *testing.T) { 46 | // FIXME use new config 47 | err := loadEnv() 48 | assert.NoError(t, err) 49 | 50 | USERNAME := os.Getenv("METERED_USERNAME") 51 | PASSWORD := os.Getenv("METERED_PASSWORD") 52 | driver := webrtcpeerconnect.Driver{ 53 | ICEServers: []webrtc.ICEServer{ 54 | { 55 | URLs: []string{"turn:standard.relay.metered.ca:80"}, 56 | Username: USERNAME, 57 | Credential: PASSWORD, 58 | }, 59 | }, // TODO more servers 60 | ICETransportPolicy: webrtc.ICETransportPolicyRelay, 61 | } 62 | 63 | specifications.ConnectToServerSpecification(t, driver) 64 | } 65 | 66 | // TODO perhaps test connecting round trip offerer and answerer 67 | -------------------------------------------------------------------------------- /acceptance-tests/server_measurements_test.go: -------------------------------------------------------------------------------- 1 | package acceptance_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | ) 8 | 9 | func TestMeasureThroughputOnMetered(t *testing.T) { 10 | err := loadEnv() 11 | assert.NoError(t, err) 12 | 13 | // FIXME use config.yaml instead? 14 | // API_KEY := os.Getenv("METERED_TURN_API_KEY") 15 | // driver := metered.Driver{} 16 | 17 | // specifications.ServerMeasurementsSpecification(t, driver, "throughput") 18 | } 19 | -------------------------------------------------------------------------------- /adapters/api/driver.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/nimbleape/iceperf-agent/adapters" 12 | "github.com/nimbleape/iceperf-agent/config" 13 | "github.com/pion/webrtc/v4" 14 | "github.com/rs/xid" 15 | ) 16 | 17 | type Driver struct { 18 | Config *config.ICEConfig 19 | Logger *slog.Logger 20 | } 21 | 22 | type ApiIceServer struct { 23 | URLs []string `json:"urls,omitempty"` 24 | Username string `json:"username,omitempty"` 25 | Credential string `json:"credential,omitempty"` 26 | } 27 | 28 | type ProviderRes struct { 29 | IceServers []ApiIceServer `json:"iceServers"` 30 | DoThroughput bool `json:"doThroughput"` 31 | } 32 | type ApiResponse struct { 33 | Providers map[string]ProviderRes `json:"providers"` 34 | Node string `json:"node"` 35 | } 36 | 37 | func (d *Driver) GetIceServers(testRunId xid.ID) (map[string]adapters.IceServersConfig, string, error) { 38 | providersAndIceServers := make(map[string]adapters.IceServersConfig) 39 | 40 | if d.Config.RequestUrl != "" { 41 | 42 | client := &http.Client{} 43 | 44 | req, err := http.NewRequest("POST", d.Config.RequestUrl, strings.NewReader(`{"testRunID": "`+testRunId.String()+`"}`)) 45 | req.Header.Add("Content-Type", "application/json") 46 | req.Header.Add("Authorization", "Bearer "+d.Config.ApiKey) 47 | 48 | if err != nil { 49 | // log.WithFields(log.Fields{ 50 | // "error": err, 51 | // }).Error("Error forming http request") 52 | return providersAndIceServers, "", err 53 | } 54 | 55 | res, err := client.Do(req) 56 | if err != nil { 57 | // log.WithFields(log.Fields{ 58 | // "error": err, 59 | // }).Error("Error doing http response") 60 | return providersAndIceServers, "", err 61 | } 62 | 63 | defer res.Body.Close() 64 | //check the code of the response 65 | if res.StatusCode != 200 { 66 | err = errors.New("error from our api") 67 | return providersAndIceServers, "", err 68 | } 69 | 70 | responseData, err := io.ReadAll(res.Body) 71 | if err != nil { 72 | // log.WithFields(log.Fields{ 73 | // "error": err, 74 | // }).Error("Error reading http response") 75 | return providersAndIceServers, "", err 76 | } 77 | // log.Info("got a response back from cloudflare api") 78 | 79 | responseServers := ApiResponse{} 80 | json.Unmarshal([]byte(responseData), &responseServers) 81 | 82 | // log.WithFields(log.Fields{ 83 | // "response": responseServers, 84 | // }).Info("http response") 85 | 86 | node := responseServers.Node 87 | 88 | for k, q := range responseServers.Providers { 89 | 90 | iceServers := []webrtc.ICEServer{} 91 | for _, r := range q.IceServers { 92 | 93 | s := webrtc.ICEServer{ 94 | URLs: r.URLs, 95 | } 96 | 97 | if r.Username != "" { 98 | s.Username = r.Username 99 | } 100 | if r.Credential != "" { 101 | s.Credential = r.Credential 102 | } 103 | iceServers = append(iceServers, s) 104 | } 105 | providersAndIceServers[k] = adapters.IceServersConfig{ 106 | DoThroughput: q.DoThroughput, 107 | IceServers: iceServers, 108 | } 109 | } 110 | return providersAndIceServers, node, nil 111 | } 112 | return providersAndIceServers, "", nil 113 | } 114 | -------------------------------------------------------------------------------- /adapters/cloudflare/driver.go: -------------------------------------------------------------------------------- 1 | package cloudflare 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/nimbleape/iceperf-agent/adapters" 13 | "github.com/nimbleape/iceperf-agent/config" 14 | "github.com/pion/stun/v2" 15 | "github.com/pion/webrtc/v4" 16 | ) 17 | 18 | type Driver struct { 19 | Config *config.ICEConfig 20 | Logger *slog.Logger 21 | } 22 | 23 | type CloudflareIceServers struct { 24 | URLs []string `json:"urls,omitempty"` 25 | Username string `json:"username,omitempty"` 26 | Credential string `json:"credential,omitempty"` 27 | } 28 | 29 | type CloudflareResponse struct { 30 | IceServers CloudflareIceServers `json:"iceServers"` 31 | } 32 | 33 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 34 | 35 | iceServers := adapters.IceServersConfig{ 36 | IceServers: []webrtc.ICEServer{}, 37 | DoThroughput: d.Config.DoThroughput, 38 | } 39 | 40 | if d.Config.RequestUrl != "" { 41 | 42 | client := &http.Client{} 43 | 44 | req, err := http.NewRequest("POST", d.Config.RequestUrl, strings.NewReader(`{"ttl": 86400}`)) 45 | req.Header.Add("Content-Type", "application/json") 46 | req.Header.Add("Authorization", "Bearer "+d.Config.ApiKey) 47 | 48 | if err != nil { 49 | // log.WithFields(log.Fields{ 50 | // "error": err, 51 | // }).Error("Error forming http request") 52 | return iceServers, err 53 | } 54 | 55 | res, err := client.Do(req) 56 | if err != nil { 57 | // log.WithFields(log.Fields{ 58 | // "error": err, 59 | // }).Error("Error doing http response") 60 | return iceServers, err 61 | } 62 | 63 | defer res.Body.Close() 64 | //check the code of the response 65 | if res.StatusCode != 201 { 66 | err = errors.New("error from cloudflare api") 67 | // log.WithFields(log.Fields{ 68 | // "code": res.StatusCode, 69 | // "error": err, 70 | // }).Error("Error status code http response") 71 | return iceServers, err 72 | } 73 | 74 | responseData, err := io.ReadAll(res.Body) 75 | if err != nil { 76 | // log.WithFields(log.Fields{ 77 | // "error": err, 78 | // }).Error("Error reading http response") 79 | return iceServers, err 80 | } 81 | // log.Info("got a response back from cloudflare api") 82 | 83 | responseServers := CloudflareResponse{} 84 | json.Unmarshal([]byte(responseData), &responseServers) 85 | 86 | // log.WithFields(log.Fields{ 87 | // "response": responseServers, 88 | // }).Info("http response") 89 | 90 | for _, r := range responseServers.IceServers.URLs { 91 | 92 | info, err := stun.ParseURI(r) 93 | 94 | if err != nil { 95 | return iceServers, err 96 | } 97 | 98 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 99 | continue 100 | } 101 | 102 | s := webrtc.ICEServer{ 103 | URLs: []string{r}, 104 | } 105 | 106 | if responseServers.IceServers.Username != "" { 107 | s.Username = responseServers.IceServers.Username 108 | } 109 | if responseServers.IceServers.Credential != "" { 110 | s.Credential = responseServers.IceServers.Credential 111 | } 112 | iceServers.IceServers = append(iceServers.IceServers, s) 113 | } 114 | } else { 115 | if d.Config.StunHost != "" && d.Config.StunEnabled { 116 | if _, ok := d.Config.StunPorts["udp"]; ok { 117 | for _, port := range d.Config.StunPorts["udp"] { 118 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 119 | URLs: []string{fmt.Sprintf("stun:%s:%d", d.Config.StunHost, port)}, 120 | }) 121 | } 122 | } 123 | } 124 | 125 | if d.Config.TurnHost != "" && d.Config.TurnEnabled { 126 | for transport := range d.Config.TurnPorts { 127 | switch transport { 128 | case "udp": 129 | for _, port := range d.Config.TurnPorts["udp"] { 130 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 131 | URLs: []string{fmt.Sprintf("turn:%s:%d?transport=udp", d.Config.TurnHost, port)}, 132 | Username: d.Config.Username, 133 | Credential: d.Config.Password, 134 | }) 135 | } 136 | case "tcp": 137 | for _, port := range d.Config.TurnPorts["tcp"] { 138 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 139 | URLs: []string{fmt.Sprintf("turn:%s:%d?transport=tcp", d.Config.TurnHost, port)}, 140 | Username: d.Config.Username, 141 | Credential: d.Config.Password, 142 | }) 143 | } 144 | case "tls": 145 | for _, port := range d.Config.TurnPorts["tls"] { 146 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 147 | URLs: []string{fmt.Sprintf("turns:%s:%d?transport=tcp", d.Config.TurnHost, port)}, 148 | Username: d.Config.Username, 149 | Credential: d.Config.Password, 150 | }) 151 | } 152 | default: 153 | } 154 | } 155 | } 156 | } 157 | return iceServers, nil 158 | } 159 | -------------------------------------------------------------------------------- /adapters/elixir/driver.go: -------------------------------------------------------------------------------- 1 | package elixir 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | 10 | "github.com/nimbleape/iceperf-agent/adapters" 11 | "github.com/nimbleape/iceperf-agent/config" 12 | "github.com/pion/stun/v2" 13 | "github.com/pion/webrtc/v4" 14 | ) 15 | 16 | type Driver struct { 17 | Config *config.ICEConfig 18 | Logger *slog.Logger 19 | } 20 | 21 | type ElixirResponse struct { 22 | Username string `json:"username"` 23 | TTL string `json:"ttl"` 24 | Password string `json:"password"` 25 | IceServers []string `json:"uris"` 26 | } 27 | 28 | // func (d Driver) Measure(measurementName string) error { 29 | // return nil 30 | // } 31 | 32 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 33 | 34 | iceServers := adapters.IceServersConfig{ 35 | IceServers: []webrtc.ICEServer{}, 36 | DoThroughput: d.Config.DoThroughput, 37 | } 38 | 39 | client := &http.Client{} 40 | req, err := http.NewRequest("POST", d.Config.RequestUrl+"&username="+d.Config.HttpUsername, nil) 41 | 42 | if err != nil { 43 | // log.WithFields(log.Fields{ 44 | // "error": err, 45 | // }).Error("Error forming http request") 46 | return iceServers, err 47 | } 48 | 49 | res, err := client.Do(req) 50 | if err != nil { 51 | // log.WithFields(log.Fields{ 52 | // "error": err, 53 | // }).Error("Error doing http response") 54 | return iceServers, err 55 | } 56 | 57 | defer res.Body.Close() 58 | //check the code of the response 59 | if res.StatusCode != 200 { 60 | err = errors.New("error from elixir api") 61 | // log.WithFields(log.Fields{ 62 | // "error": err, 63 | // }).Error("Error status code http response") 64 | return iceServers, err 65 | } 66 | 67 | responseData, err := io.ReadAll(res.Body) 68 | if err != nil { 69 | // log.WithFields(log.Fields{ 70 | // "error": err, 71 | // }).Error("Error reading http response") 72 | return iceServers, err 73 | } 74 | 75 | responseServers := ElixirResponse{} 76 | json.Unmarshal([]byte(responseData), &responseServers) 77 | 78 | for _, r := range responseServers.IceServers { 79 | 80 | info, err := stun.ParseURI(r) 81 | 82 | if err != nil { 83 | return iceServers, err 84 | } 85 | 86 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 87 | continue 88 | } 89 | 90 | s := webrtc.ICEServer{ 91 | URLs: []string{r}, 92 | } 93 | 94 | if responseServers.Username != "" { 95 | s.Username = responseServers.Username 96 | } 97 | if responseServers.Password != "" { 98 | s.Credential = responseServers.Password 99 | } 100 | 101 | iceServers.IceServers = append(iceServers.IceServers, s) 102 | 103 | if d.Config.StunEnabled { 104 | stun := webrtc.ICEServer{ 105 | URLs: []string{"stun:" + info.Host + ":3478"}, 106 | } 107 | iceServers.IceServers = append(iceServers.IceServers, stun) 108 | } 109 | } 110 | 111 | // 112 | 113 | return iceServers, nil 114 | } 115 | -------------------------------------------------------------------------------- /adapters/expressturn/driver.go: -------------------------------------------------------------------------------- 1 | package expressturn 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/nimbleape/iceperf-agent/adapters" 8 | "github.com/nimbleape/iceperf-agent/config" 9 | "github.com/pion/webrtc/v4" 10 | ) 11 | 12 | type Driver struct { 13 | Config *config.ICEConfig 14 | Logger *slog.Logger 15 | } 16 | 17 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 18 | 19 | iceServers := adapters.IceServersConfig{ 20 | IceServers: []webrtc.ICEServer{}, 21 | } 22 | 23 | if d.Config.StunHost != "" && d.Config.StunEnabled { 24 | if _, ok := d.Config.StunPorts["udp"]; ok { 25 | for _, port := range d.Config.StunPorts["udp"] { 26 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 27 | URLs: []string{fmt.Sprintf("stun:%s:%d", d.Config.StunHost, port)}, 28 | }) 29 | } 30 | } 31 | } 32 | 33 | if d.Config.TurnHost != "" && d.Config.TurnEnabled { 34 | for transport := range d.Config.TurnPorts { 35 | switch transport { 36 | case "udp": 37 | for _, port := range d.Config.TurnPorts["udp"] { 38 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 39 | URLs: []string{fmt.Sprintf("turn:%s:%d?transport=udp", d.Config.TurnHost, port)}, 40 | Username: d.Config.Username, 41 | Credential: d.Config.Password, 42 | }) 43 | } 44 | case "tcp": 45 | for _, port := range d.Config.TurnPorts["tcp"] { 46 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 47 | URLs: []string{fmt.Sprintf("turn:%s:%d?transport=tcp", d.Config.TurnHost, port)}, 48 | Username: d.Config.Username, 49 | Credential: d.Config.Password, 50 | }) 51 | } 52 | case "tls": 53 | for _, port := range d.Config.TurnPorts["tls"] { 54 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 55 | URLs: []string{fmt.Sprintf("turns:%s:%d?transport=tcp", d.Config.TurnHost, port)}, 56 | Username: d.Config.Username, 57 | Credential: d.Config.Password, 58 | }) 59 | } 60 | default: 61 | } 62 | } 63 | } 64 | return iceServers, nil 65 | } 66 | -------------------------------------------------------------------------------- /adapters/google/driver.go: -------------------------------------------------------------------------------- 1 | package google 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/nimbleape/iceperf-agent/adapters" 8 | "github.com/nimbleape/iceperf-agent/config" 9 | "github.com/pion/webrtc/v4" 10 | ) 11 | 12 | type Driver struct { 13 | Config *config.ICEConfig 14 | Logger *slog.Logger 15 | } 16 | 17 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 18 | 19 | iceServers := adapters.IceServersConfig{ 20 | IceServers: []webrtc.ICEServer{}, 21 | } 22 | 23 | if d.Config.StunHost != "" && d.Config.StunEnabled { 24 | if _, ok := d.Config.StunPorts["udp"]; ok { 25 | for _, port := range d.Config.StunPorts["udp"] { 26 | iceServers.IceServers = append(iceServers.IceServers, webrtc.ICEServer{ 27 | URLs: []string{fmt.Sprintf("stun:%s:%d", d.Config.StunHost, port)}, 28 | }) 29 | } 30 | } 31 | } 32 | return iceServers, nil 33 | } 34 | -------------------------------------------------------------------------------- /adapters/metered/driver.go: -------------------------------------------------------------------------------- 1 | package metered 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/nimbleape/iceperf-agent/adapters" 10 | "github.com/nimbleape/iceperf-agent/config" 11 | "github.com/pion/stun/v2" 12 | "github.com/pion/webrtc/v4" 13 | ) 14 | 15 | type Driver struct { 16 | Config *config.ICEConfig 17 | Logger *slog.Logger 18 | } 19 | 20 | type MeteredIceServers struct { 21 | URLs string `json:"urls,omitempty"` 22 | Username string `json:"username,omitempty"` 23 | Credential string `json:"credential,omitempty"` 24 | } 25 | 26 | // func (d Driver) Measure(measurementName string) error { 27 | // return nil 28 | // } 29 | 30 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 31 | 32 | iceServers := adapters.IceServersConfig{ 33 | IceServers: []webrtc.ICEServer{}, 34 | } 35 | 36 | res, err := http.Get(d.Config.RequestUrl + "?apiKey=" + d.Config.ApiKey) 37 | if err != nil { 38 | // log.WithFields(log.Fields{ 39 | // "error": err, 40 | // }).Error("Error making http request") 41 | return iceServers, err 42 | } 43 | 44 | responseData, err := io.ReadAll(res.Body) 45 | if err != nil { 46 | // log.WithFields(log.Fields{ 47 | // "error": err, 48 | // }).Error("Error reading http response") 49 | return iceServers, err 50 | } 51 | 52 | var responseServers []MeteredIceServers 53 | json.Unmarshal([]byte(responseData), &responseServers) 54 | 55 | gotTransports := make(map[string]bool) 56 | 57 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeUDP.String()] = false 58 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeTCP.String()] = false 59 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeUDP.String()] = false 60 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeTCP.String()] = false 61 | gotTransports[stun.SchemeTypeTURNS.String()+stun.ProtoTypeTCP.String()] = false 62 | 63 | for _, r := range responseServers { 64 | 65 | info, err := stun.ParseURI(r.URLs) 66 | 67 | if err != nil { 68 | return iceServers, err 69 | } 70 | 71 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 72 | continue 73 | } 74 | 75 | if gotTransports[info.Scheme.String()+info.Proto.String()] { 76 | //we don't want to test all the special ports right now 77 | continue 78 | } 79 | 80 | s := webrtc.ICEServer{ 81 | URLs: []string{r.URLs}, 82 | } 83 | 84 | if r.Username != "" { 85 | s.Username = r.Username 86 | } 87 | if r.Credential != "" { 88 | s.Credential = r.Credential 89 | } 90 | iceServers.IceServers = append(iceServers.IceServers, s) 91 | gotTransports[info.Scheme.String()+info.Proto.String()] = true 92 | } 93 | return iceServers, nil 94 | } 95 | -------------------------------------------------------------------------------- /adapters/serverconnect/driver.go: -------------------------------------------------------------------------------- 1 | package serverconnect 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type Driver struct { 10 | Url string 11 | Client *http.Client 12 | } 13 | 14 | // FIXME this is actually just a "Connect to TURN provider" test 15 | /* 16 | This handles the actual connection to the TURN server. 17 | Here we call the TURN provider and consider our client connected 18 | if we receive a list of ICE Servers. 19 | */ 20 | func (d Driver) Connect() (connected bool, err error) { 21 | res, err := d.Client.Get(d.Url) 22 | if err != nil { 23 | return false, err 24 | } 25 | defer res.Body.Close() 26 | 27 | responseData, err := io.ReadAll(res.Body) 28 | if err != nil { 29 | return false, err 30 | } 31 | 32 | var responseServers []map[string]interface{} 33 | json.Unmarshal([]byte(responseData), &responseServers) 34 | 35 | return len(responseServers) > 0, nil 36 | } 37 | -------------------------------------------------------------------------------- /adapters/stunner/driver.go: -------------------------------------------------------------------------------- 1 | package stunner 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | 10 | "github.com/nimbleape/iceperf-agent/adapters" 11 | "github.com/nimbleape/iceperf-agent/config" 12 | "github.com/pion/stun/v2" 13 | "github.com/pion/webrtc/v4" 14 | ) 15 | 16 | type Driver struct { 17 | Config *config.ICEConfig 18 | Logger *slog.Logger 19 | } 20 | 21 | type IceServer struct { 22 | Credential string `json:"credential"` 23 | Urls []string `json:"urls"` 24 | Username string `json:"username"` 25 | } 26 | 27 | type StunnerResponse struct { 28 | IceServers []IceServer `json:"iceServers"` 29 | IceTransportPolicy string `json:"iceTransportPolicy"` 30 | } 31 | 32 | // func (d Driver) Measure(measurementName string) error { 33 | // return nil 34 | // } 35 | 36 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 37 | 38 | iceServers := adapters.IceServersConfig{ 39 | IceServers: []webrtc.ICEServer{}, 40 | DoThroughput: d.Config.DoThroughput, 41 | } 42 | 43 | client := &http.Client{} 44 | req, err := http.NewRequest("GET", d.Config.RequestUrl, nil) 45 | 46 | if err != nil { 47 | // log.WithFields(log.Fields{ 48 | // "error": err, 49 | // }).Error("Error forming http request") 50 | return iceServers, err 51 | } 52 | 53 | res, err := client.Do(req) 54 | if err != nil { 55 | // log.WithFields(log.Fields{ 56 | // "error": err, 57 | // }).Error("Error doing http response") 58 | return iceServers, err 59 | } 60 | 61 | defer res.Body.Close() 62 | //check the code of the response 63 | if res.StatusCode != 200 { 64 | err = errors.New("error from Stunner api") 65 | // log.WithFields(log.Fields{ 66 | // "error": err, 67 | // }).Error("Error status code http response") 68 | return iceServers, err 69 | } 70 | 71 | responseData, err := io.ReadAll(res.Body) 72 | if err != nil { 73 | // log.WithFields(log.Fields{ 74 | // "error": err, 75 | // }).Error("Error reading http response") 76 | return iceServers, err 77 | } 78 | 79 | responseServers := StunnerResponse{} 80 | json.Unmarshal([]byte(responseData), &responseServers) 81 | 82 | for _, server := range responseServers.IceServers { 83 | for _, url := range server.Urls { 84 | info, err := stun.ParseURI(url) 85 | if err != nil { 86 | return iceServers, err 87 | } 88 | 89 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 90 | continue 91 | } 92 | 93 | s := webrtc.ICEServer{ 94 | URLs: []string{url}, 95 | Username: server.Username, 96 | Credential: server.Credential, 97 | } 98 | 99 | iceServers.IceServers = append(iceServers.IceServers, s) 100 | 101 | if d.Config.StunEnabled { 102 | stun := webrtc.ICEServer{ 103 | URLs: []string{"stun:" + info.Host + ":3478"}, 104 | } 105 | iceServers.IceServers = append(iceServers.IceServers, stun) 106 | } 107 | } 108 | } 109 | 110 | return iceServers, nil 111 | } -------------------------------------------------------------------------------- /adapters/twilio/driver.go: -------------------------------------------------------------------------------- 1 | package twilio 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | 10 | "github.com/nimbleape/iceperf-agent/adapters" 11 | "github.com/nimbleape/iceperf-agent/config" 12 | "github.com/pion/stun/v2" 13 | "github.com/pion/webrtc/v4" 14 | // log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Driver struct { 18 | Config *config.ICEConfig 19 | Logger *slog.Logger 20 | } 21 | 22 | type TwilioIceServers struct { 23 | URL string `json:"url,omitempty"` 24 | URLs string `json:"urls,omitempty"` 25 | Username string `json:"username,omitempty"` 26 | Credential string `json:"credential,omitempty"` 27 | } 28 | type TwilioResponse struct { 29 | Username string `json:"username"` 30 | DateUpdated string `json:"date_updated"` 31 | TTL string `json:"ttl"` 32 | DateCreated string `json:"date_created"` 33 | Password string `json:"password"` 34 | IceServers []TwilioIceServers `json:"ice_servers"` 35 | } 36 | 37 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 38 | client := &http.Client{} 39 | 40 | iceServers := adapters.IceServersConfig{ 41 | IceServers: []webrtc.ICEServer{}, 42 | } 43 | 44 | req, err := http.NewRequest("POST", d.Config.RequestUrl, nil) 45 | req.SetBasicAuth(d.Config.HttpUsername, d.Config.HttpPassword) 46 | 47 | if err != nil { 48 | // log.WithFields(log.Fields{ 49 | // "error": err, 50 | // }).Error("Error forming http request") 51 | return iceServers, err 52 | } 53 | 54 | res, err := client.Do(req) 55 | if err != nil { 56 | // log.WithFields(log.Fields{ 57 | // "error": err, 58 | // }).Error("Error doing http response") 59 | return iceServers, err 60 | } 61 | 62 | defer res.Body.Close() 63 | //check the code of the response 64 | if res.StatusCode != 201 { 65 | err = errors.New("error from twilio api") 66 | // log.WithFields(log.Fields{ 67 | // "error": err, 68 | // }).Error("Error status code http response") 69 | return iceServers, err 70 | } 71 | 72 | responseData, err := io.ReadAll(res.Body) 73 | if err != nil { 74 | // log.WithFields(log.Fields{ 75 | // "error": err, 76 | // }).Error("Error reading http response") 77 | return iceServers, err 78 | } 79 | 80 | responseServers := TwilioResponse{} 81 | json.Unmarshal([]byte(responseData), &responseServers) 82 | 83 | tempTurnHost := "" 84 | 85 | gotTransports := make(map[string]bool) 86 | 87 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeUDP.String()] = false 88 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeTCP.String()] = false 89 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeUDP.String()] = false 90 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeTCP.String()] = false 91 | gotTransports[stun.SchemeTypeTURNS.String()+stun.ProtoTypeTCP.String()] = false 92 | 93 | for _, r := range responseServers.IceServers { 94 | 95 | info, err := stun.ParseURI(r.URL) 96 | 97 | if err != nil { 98 | return iceServers, err 99 | } 100 | 101 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 102 | continue 103 | } 104 | 105 | if gotTransports[info.Scheme.String()+info.Proto.String()] { 106 | //we don't want to test all the special ports right now 107 | continue 108 | } 109 | 110 | if info.Scheme == stun.SchemeTypeTURN { 111 | tempTurnHost = info.Host 112 | } 113 | 114 | s := webrtc.ICEServer{ 115 | URLs: []string{r.URL}, 116 | } 117 | 118 | if r.Username != "" { 119 | s.Username = r.Username 120 | } 121 | if r.Credential != "" { 122 | s.Credential = r.Credential 123 | } 124 | iceServers.IceServers = append(iceServers.IceServers, s) 125 | gotTransports[info.Scheme.String()+info.Proto.String()] = true 126 | } 127 | 128 | if d.Config.TurnEnabled { 129 | //apparently if you go and make a tls turn uri it will work 130 | s := webrtc.ICEServer{ 131 | URLs: []string{"turns:" + tempTurnHost + ":5349?transport=tcp"}, 132 | } 133 | 134 | if responseServers.Username != "" { 135 | s.Username = responseServers.Username 136 | } 137 | if responseServers.Password != "" { 138 | s.Credential = responseServers.Password 139 | } 140 | 141 | iceServers.IceServers = append(iceServers.IceServers, s) 142 | } 143 | 144 | return iceServers, nil 145 | } 146 | -------------------------------------------------------------------------------- /adapters/types.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import "github.com/pion/webrtc/v4" 4 | 5 | type IceServersConfig struct { 6 | IceServers []webrtc.ICEServer 7 | DoThroughput bool 8 | } 9 | -------------------------------------------------------------------------------- /adapters/webrtcpeerconnect/driver.go: -------------------------------------------------------------------------------- 1 | package webrtcpeerconnect 2 | 3 | import ( 4 | "github.com/pion/webrtc/v4" 5 | ) 6 | 7 | type Driver struct { 8 | ICEServers []webrtc.ICEServer 9 | ICETransportPolicy webrtc.ICETransportPolicy 10 | } 11 | 12 | func (d Driver) Connect() (connected bool, err error) { 13 | // FIXME use new config 14 | // config := client.NewClientConfig() 15 | // config.WebRTCConfig.ICEServers = d.ICEServers 16 | // config.WebRTCConfig.ICETransportPolicy = d.ICETransportPolicy 17 | 18 | // c, err := client.NewClient(config) 19 | // if err != nil { 20 | // return false, err 21 | // } 22 | // c.Run() 23 | 24 | // state := <-c.OffererConnected 25 | // return state, nil 26 | return true, nil 27 | } 28 | -------------------------------------------------------------------------------- /adapters/xirsys/driver.go: -------------------------------------------------------------------------------- 1 | package xirsys 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/nimbleape/iceperf-agent/adapters" 12 | "github.com/nimbleape/iceperf-agent/config" 13 | "github.com/pion/stun/v2" 14 | "github.com/pion/webrtc/v4" 15 | // log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | type Driver struct { 19 | Config *config.ICEConfig 20 | Logger *slog.Logger 21 | } 22 | 23 | type XirsysIceServers struct { 24 | URLs []string `json:"urls,omitempty"` 25 | Username string `json:"username,omitempty"` 26 | Credential string `json:"credential,omitempty"` 27 | } 28 | 29 | type XirsysIceServer struct { 30 | IceServers XirsysIceServers `json:"iceServers"` 31 | } 32 | type XirsysResponse struct { 33 | V XirsysIceServer `json:"v"` 34 | S string `json:"s"` 35 | } 36 | 37 | func (d *Driver) GetIceServers() (adapters.IceServersConfig, error) { 38 | client := &http.Client{} 39 | 40 | iceServers := adapters.IceServersConfig{ 41 | IceServers: []webrtc.ICEServer{}, 42 | } 43 | 44 | req, err := http.NewRequest("PUT", d.Config.RequestUrl, strings.NewReader(`{"format": "urls", "expire": "1800"}`)) 45 | req.SetBasicAuth(d.Config.HttpUsername, d.Config.HttpPassword) 46 | req.Header.Add("Content-Type", "application/json") 47 | 48 | if err != nil { 49 | // log.WithFields(log.Fields{ 50 | // "error": err, 51 | // }).Error("Error forming http request") 52 | return iceServers, err 53 | } 54 | 55 | res, err := client.Do(req) 56 | if err != nil { 57 | // log.WithFields(log.Fields{ 58 | // "error": err, 59 | // }).Error("Error doing http response") 60 | return iceServers, err 61 | } 62 | 63 | defer res.Body.Close() 64 | //check the code of the response 65 | if res.StatusCode != 200 { 66 | err = errors.New("error from xirsys api") 67 | // log.WithFields(log.Fields{ 68 | // "error": err, 69 | // "status": res.StatusCode, 70 | // }).Error("Error status code http response") 71 | return iceServers, err 72 | } 73 | 74 | responseData, err := io.ReadAll(res.Body) 75 | if err != nil { 76 | // log.WithFields(log.Fields{ 77 | // "error": err, 78 | // }).Error("Error reading http response") 79 | return iceServers, err 80 | } 81 | 82 | responseServers := XirsysResponse{} 83 | json.Unmarshal([]byte(responseData), &responseServers) 84 | 85 | gotTransports := make(map[string]bool) 86 | 87 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeUDP.String()] = false 88 | gotTransports[stun.SchemeTypeSTUN.String()+stun.ProtoTypeTCP.String()] = false 89 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeUDP.String()] = false 90 | gotTransports[stun.SchemeTypeTURN.String()+stun.ProtoTypeTCP.String()] = false 91 | gotTransports[stun.SchemeTypeTURNS.String()+stun.ProtoTypeTCP.String()] = false 92 | 93 | for _, r := range responseServers.V.IceServers.URLs { 94 | 95 | info, err := stun.ParseURI(r) 96 | 97 | if err != nil { 98 | return iceServers, err 99 | } 100 | 101 | if ((info.Scheme == stun.SchemeTypeTURN || info.Scheme == stun.SchemeTypeTURNS) && !d.Config.TurnEnabled) || ((info.Scheme == stun.SchemeTypeSTUN || info.Scheme == stun.SchemeTypeSTUNS) && !d.Config.StunEnabled) { 102 | continue 103 | } 104 | 105 | if gotTransports[info.Scheme.String()+info.Proto.String()] { 106 | //we don't want to test all the special ports right now 107 | continue 108 | } 109 | 110 | s := webrtc.ICEServer{ 111 | URLs: []string{r}, 112 | } 113 | 114 | if responseServers.V.IceServers.Username != "" { 115 | s.Username = responseServers.V.IceServers.Username 116 | } 117 | if responseServers.V.IceServers.Credential != "" { 118 | s.Credential = responseServers.V.IceServers.Credential 119 | } 120 | iceServers.IceServers = append(iceServers.IceServers, s) 121 | gotTransports[info.Scheme.String()+info.Proto.String()] = true 122 | } 123 | 124 | return iceServers, nil 125 | } 126 | -------------------------------------------------------------------------------- /assets/ICEPerf_fulllogo_nobuffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/everycastlabs/iceperf-agent/3ee2494d19e4e5bbfd150d6cec1b9471396e820d/assets/ICEPerf_fulllogo_nobuffer.png -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/nimbleape/iceperf-agent/config" 12 | "github.com/nimbleape/iceperf-agent/stats" 13 | "github.com/nimbleape/iceperf-agent/util" 14 | "github.com/pion/stun/v2" 15 | "github.com/pion/webrtc/v4" 16 | "github.com/rs/xid" 17 | // "github.com/prometheus/client_golang/prometheus" 18 | // log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var ( 22 | startTime time.Time 23 | timeAnswererReceivedCandidate time.Time 24 | timeOffererReceivedCandidate time.Time 25 | timeAnswererConnecting time.Time 26 | timeAnswererConnected time.Time 27 | timeOffererConnecting time.Time 28 | timeOffererConnected time.Time 29 | bufferedAmountLowThreshold uint64 = 512 * 1024 30 | maxBufferedAmount uint64 = 1024 * 1024 // 1 MiB 31 | ) 32 | 33 | type Client struct { 34 | ConnectionPair *ConnectionPair 35 | OffererConnected chan bool 36 | AnswererConnected chan bool 37 | close chan struct{} 38 | Logger *slog.Logger 39 | provider string 40 | Stats *stats.Stats 41 | config *config.Config 42 | } 43 | 44 | func NewClient(config *config.Config, iceServerInfo *stun.URI, provider string, testRunId xid.ID, testRunStartedAt time.Time, doThroughputTest bool, close chan struct{}) (c *Client, err error) { 45 | return newClient(config, iceServerInfo, provider, testRunId, testRunStartedAt, doThroughputTest, close) 46 | } 47 | 48 | func newClient(cc *config.Config, iceServerInfo *stun.URI, provider string, testRunId xid.ID, testRunStartedAt time.Time, doThroughputTest bool, close chan struct{}) (*Client, error) { 49 | 50 | // Start timers 51 | startTime = time.Now() 52 | 53 | stats := stats.NewStats(testRunId.String(), testRunStartedAt) 54 | 55 | stats.SetProvider(provider) 56 | stats.SetScheme(iceServerInfo.Scheme.String()) 57 | stats.SetProtocol(iceServerInfo.Proto.String()) 58 | stats.SetPort(fmt.Sprintf("%d", iceServerInfo.Port)) 59 | stats.SetNode(cc.NodeID) 60 | 61 | connectionPair, err := newConnectionPair(cc, iceServerInfo, provider, stats, doThroughputTest, close) 62 | 63 | if doThroughputTest { 64 | bufferedAmountLowThreshold = 4 * 1024 * 1024 // 4 Mib 65 | maxBufferedAmount = 8 * 1024 * 1024 // 8 Mib 66 | } 67 | 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | c := &Client{ 73 | ConnectionPair: connectionPair, 74 | OffererConnected: make(chan bool), 75 | AnswererConnected: make(chan bool), 76 | close: make(chan struct{}), 77 | Logger: cc.Logger, 78 | provider: provider, 79 | Stats: stats, 80 | config: cc, 81 | } 82 | 83 | if cc.OnICECandidate != nil { 84 | c.ConnectionPair.AnswerPC.OnICECandidate(cc.OnICECandidate) 85 | c.ConnectionPair.OfferPC.OnICECandidate(cc.OnICECandidate) 86 | } else { 87 | // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate 88 | // send it to the other peer 89 | 90 | //if the test is a STUN test, then allow host, srflx and relay candidates 91 | c.ConnectionPair.AnswerPC.OnICECandidate(func(i *webrtc.ICECandidate) { 92 | if i != nil { 93 | if i.Typ == webrtc.ICECandidateTypeSrflx || i.Typ == webrtc.ICECandidateTypeRelay || (i.Typ == webrtc.ICECandidateTypeHost && (iceServerInfo.Scheme == stun.SchemeTypeSTUN || iceServerInfo.Scheme == stun.SchemeTypeSTUNS)) { 94 | stats.SetAnswererTimeToReceiveCandidate(float64(time.Since(startTime).Milliseconds())) 95 | timeAnswererReceivedCandidate = time.Now() 96 | c.ConnectionPair.LogAnswerer.Info("Answerer received candidate, sent over to other PC", "eventTime", timeAnswererReceivedCandidate, 97 | "timeSinceStartMs", time.Since(startTime).Milliseconds(), 98 | "candidateType", i.Typ, 99 | "relayAddress", i.RelatedAddress, 100 | "relayPort", i.RelatedPort) 101 | util.Check(c.ConnectionPair.OfferPC.AddICECandidate(i.ToJSON())) 102 | } 103 | } 104 | }) 105 | 106 | // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate 107 | // send it to the other peer 108 | c.ConnectionPair.OfferPC.OnICECandidate(func(i *webrtc.ICECandidate) { 109 | if i != nil { 110 | if i.Typ == webrtc.ICECandidateTypeSrflx || i.Typ == webrtc.ICECandidateTypeRelay { 111 | stats.SetOffererTimeToReceiveCandidate(float64(time.Since(startTime).Milliseconds())) 112 | timeOffererReceivedCandidate = time.Now() 113 | c.ConnectionPair.LogOfferer.Info("Offerer received candidate, sent over to other PC", "eventTime", timeOffererReceivedCandidate, 114 | "timeSinceStartMs", time.Since(startTime).Milliseconds(), 115 | "candidateType", i.Typ, 116 | "relayAddress", i.RelatedAddress, 117 | "relayPort", i.RelatedPort) 118 | util.Check(c.ConnectionPair.AnswerPC.AddICECandidate(i.ToJSON())) 119 | } 120 | } 121 | }) 122 | } 123 | 124 | if cc.OnConnectionStateChange != nil { 125 | c.ConnectionPair.OfferPC.OnConnectionStateChange(cc.OnConnectionStateChange) 126 | c.ConnectionPair.AnswerPC.OnConnectionStateChange(cc.OnConnectionStateChange) 127 | } else { 128 | // Set the handler for Peer connection state 129 | // This will notify you when the peer has connected/disconnected 130 | c.ConnectionPair.OfferPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 131 | c.ConnectionPair.LogOfferer.Info("Peer Connection State has changed", "eventTime", time.Now(), 132 | "peerConnState", s.String()) 133 | 134 | switch s { 135 | case webrtc.PeerConnectionStateConnecting: 136 | timeOffererConnecting = time.Now() 137 | c.ConnectionPair.LogOfferer.Info("Offerer connecting", "eventTime", timeOffererConnecting, 138 | "timeSinceStartMs", time.Since(startTime).Milliseconds()) 139 | case webrtc.PeerConnectionStateConnected: 140 | timeOffererConnected = time.Now() 141 | c.ConnectionPair.LogOfferer.Info("Offerer connected", "eventTime", timeOffererConnected, "timeSinceStartMs", time.Since(startTime).Milliseconds()) 142 | //go and get the details about the ice pair 143 | //stats := c.ConnectionPair.OfferPC.GetStats() 144 | // connStats, ok := stats.GetConnectionStats(c.ConnectionPair.OfferPC) 145 | // if (ok) { 146 | // c.ConnectionPair.LogOfferer.WithFields(log.Fields{ 147 | // "connStats": connStats, 148 | // "eventTime": timeOffererConnected, 149 | // "timeSinceStartMs": time.Since(startTime).Milliseconds(), 150 | // }).Info("Offerer Stats") 151 | // } 152 | // find the active candidate pair 153 | // for k, v := range stats { 154 | // c.ConnectionPair.LogOfferer.WithFields(log.Fields{ 155 | // "statsKey": k, 156 | // "statsValue": v, 157 | // "eventTime": timeOffererConnected, 158 | // "timeSinceStartMs": time.Since(startTime).Milliseconds(), 159 | // }).Info("Offerer Stats") 160 | // } 161 | stats.SetTimeToConnectedState(time.Since(startTime).Milliseconds()) 162 | c.OffererConnected <- true 163 | case webrtc.PeerConnectionStateFailed: 164 | // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. 165 | // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. 166 | // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. 167 | c.ConnectionPair.LogOfferer.Error("Offerer connection failed", "eventTime", time.Now(), "timeSinceStartMs", time.Since(startTime).Milliseconds()) 168 | close <- struct{}{} 169 | c.OffererConnected <- false 170 | case webrtc.PeerConnectionStateClosed: 171 | // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify 172 | c.ConnectionPair.LogOfferer.Info("Offerer connection closed", "eventTime", time.Now(), "timeSinceStartMs", time.Since(startTime).Milliseconds()) 173 | c.OffererConnected <- false 174 | } 175 | }) 176 | 177 | // Set the handler for Peer connection state 178 | // This will notify you when the peer has connected/disconnected 179 | c.ConnectionPair.AnswerPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 180 | c.ConnectionPair.LogAnswerer.Info("Peer Connection State has changed", "eventTime", time.Now(), "peerConnState", s.String()) 181 | 182 | switch s { 183 | case webrtc.PeerConnectionStateConnecting: 184 | timeAnswererConnecting = time.Now() 185 | c.ConnectionPair.LogAnswerer.Info("Answerer connecting", "eventTime", timeAnswererConnecting, "timeSinceStartMs", time.Since(startTime).Milliseconds()) 186 | case webrtc.PeerConnectionStateConnected: 187 | timeAnswererConnected = time.Now() 188 | c.ConnectionPair.LogAnswerer.Info("Answerer connected", "eventTime", timeAnswererConnected, "timeSinceStartMs", time.Since(startTime).Milliseconds()) 189 | c.AnswererConnected <- true 190 | case webrtc.PeerConnectionStateFailed: 191 | // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. 192 | // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. 193 | // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. 194 | c.ConnectionPair.LogAnswerer.Error("Answerer connection failed", "eventTime", time.Now(), "timeSinceStartMs", time.Since(startTime).Milliseconds()) 195 | c.AnswererConnected <- false 196 | case webrtc.PeerConnectionStateClosed: 197 | // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify 198 | c.ConnectionPair.LogAnswerer.Info("Answerer connection closed", "eventTime", time.Now(), "timeSinceStartMs", time.Since(startTime).Milliseconds()) 199 | c.AnswererConnected <- false 200 | } 201 | }) 202 | } 203 | 204 | return c, nil 205 | } 206 | 207 | func (c *Client) Run() { 208 | go c.run() 209 | } 210 | 211 | func (c *Client) run() { 212 | offer, err := c.ConnectionPair.OfferPC.CreateOffer(nil) 213 | util.Check(err) 214 | util.Check(c.ConnectionPair.OfferPC.SetLocalDescription(offer)) 215 | desc, err := json.Marshal(offer) 216 | util.Check(err) 217 | 218 | c.ConnectionPair.setRemoteDescription(c.ConnectionPair.AnswerPC, desc) 219 | 220 | answer, err := c.ConnectionPair.AnswerPC.CreateAnswer(nil) 221 | util.Check(err) 222 | util.Check(c.ConnectionPair.AnswerPC.SetLocalDescription(answer)) 223 | desc2, err := json.Marshal(answer) 224 | util.Check(err) 225 | 226 | c.ConnectionPair.setRemoteDescription(c.ConnectionPair.OfferPC, desc2) 227 | 228 | // this is blocking 229 | c.close <- struct{}{} 230 | 231 | util.Check(c.Stop()) 232 | } 233 | 234 | func (c *Client) Stop() error { 235 | c.Logger.Info("Stopping client...") 236 | 237 | if c.ConnectionPair.OfferDC != nil { 238 | c.ConnectionPair.OfferDC.Close() 239 | } 240 | 241 | time.Sleep(1 * time.Second) 242 | 243 | if err := c.ConnectionPair.OfferPC.Close(); err != nil { 244 | c.Logger.Error("cannot close c.ConnectionPair.OfferPC", "error", err) 245 | return err 246 | } 247 | 248 | if err := c.ConnectionPair.AnswerPC.Close(); err != nil { 249 | c.Logger.Error("cannot close c.ConnectionPair.AnswerPC", "error", err) 250 | return err 251 | } 252 | 253 | if c.config.Logging.API.Enabled { 254 | // Convert data to JSON 255 | c.Stats.CreateLabels() 256 | jsonData, err := json.Marshal(c.Stats) 257 | if err != nil { 258 | fmt.Println("Error marshalling JSON:", err) 259 | return err 260 | } 261 | 262 | // Define the API endpoint 263 | apiEndpoint := c.config.Logging.API.URI 264 | 265 | // Create a new HTTP request 266 | req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(jsonData)) 267 | if err != nil { 268 | fmt.Println("Error creating request:", err) 269 | return err 270 | } 271 | 272 | // Set the appropriate headers 273 | req.Header.Set("Content-Type", "application/json") 274 | req.Header.Add("Authorization", "Bearer "+c.config.Logging.API.ApiKey) 275 | 276 | // Send the request using the HTTP client 277 | client := &http.Client{} 278 | resp, err := client.Do(req) 279 | if err != nil { 280 | fmt.Println("Error sending request:", err) 281 | return err 282 | } 283 | defer resp.Body.Close() 284 | 285 | // Check the response 286 | if resp.StatusCode == http.StatusCreated { 287 | fmt.Println("Data sent successfully!") 288 | } else { 289 | fmt.Printf("Failed to send data. Status code: %d\n", resp.StatusCode) 290 | } 291 | } 292 | j, _ := c.Stats.ToJSON() 293 | c.Logger.Info(j, "individual_test_completed", "true") 294 | 295 | return nil 296 | } 297 | -------------------------------------------------------------------------------- /client/dataChannel.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/nimbleape/iceperf-agent/config" 9 | "github.com/nimbleape/iceperf-agent/stats" 10 | "github.com/nimbleape/iceperf-agent/util" 11 | "github.com/pion/stun/v2" 12 | "github.com/pion/webrtc/v4" 13 | ) 14 | 15 | type PC struct { 16 | pc *webrtc.PeerConnection 17 | } 18 | 19 | func (pc *PC) Stop() { 20 | 21 | } 22 | 23 | type ConnectionPair struct { 24 | OfferPC *webrtc.PeerConnection 25 | OfferDC *webrtc.DataChannel 26 | AnswerPC *webrtc.PeerConnection 27 | LogOfferer *slog.Logger 28 | LogAnswerer *slog.Logger 29 | config *config.Config 30 | sentInitialMessageViaDC time.Time 31 | iceServerInfo *stun.URI 32 | provider string 33 | stats *stats.Stats 34 | doThroughputTest bool 35 | closeChan chan struct{} 36 | } 37 | 38 | func NewConnectionPair(config *config.Config, iceServerInfo *stun.URI, provider string, stats *stats.Stats, doThroughputTest bool, closeChan chan struct{}) (c *ConnectionPair, err error) { 39 | return newConnectionPair(config, iceServerInfo, provider, stats, doThroughputTest, closeChan) 40 | } 41 | 42 | func newConnectionPair(cc *config.Config, iceServerInfo *stun.URI, provider string, stats *stats.Stats, doThroughputTest bool, closeChan chan struct{}) (*ConnectionPair, error) { 43 | logOfferer := cc.Logger.With("peer", "Offerer") 44 | logAnswerer := cc.Logger.With("peer", "Answerer") 45 | 46 | cp := &ConnectionPair{ 47 | config: cc, 48 | LogOfferer: logOfferer, 49 | LogAnswerer: logAnswerer, 50 | iceServerInfo: iceServerInfo, 51 | provider: provider, 52 | stats: stats, 53 | doThroughputTest: doThroughputTest, 54 | closeChan: closeChan, 55 | } 56 | 57 | config := webrtc.Configuration{} 58 | 59 | if cc.WebRTCConfig.ICEServers != nil { 60 | config.ICEServers = cc.WebRTCConfig.ICEServers 61 | } 62 | 63 | config.ICETransportPolicy = cc.WebRTCConfig.ICETransportPolicy 64 | config.SDPSemantics = webrtc.SDPSemanticsUnifiedPlanWithFallback 65 | 66 | //we only want offerer to force turn (if we are) 67 | cp.createOfferer(config) 68 | 69 | // think we want to leave the answerer without any ice servers so we only get the host candidates.... I think 70 | // to get the tests working I'm passing the turn server into both.... 71 | // but I don't think that should be required 72 | cp.createAnswerer(webrtc.Configuration{ 73 | ICEServers: []webrtc.ICEServer{ 74 | { 75 | URLs: []string{"stun:stun.l.google.com:19302"}, 76 | }, 77 | }, 78 | }) 79 | 80 | return cp, nil 81 | } 82 | 83 | func (cp *ConnectionPair) setRemoteDescription(pc *webrtc.PeerConnection, sdp []byte) { 84 | var desc webrtc.SessionDescription 85 | err := json.Unmarshal(sdp, &desc) 86 | util.Check(err) 87 | 88 | // Apply the desc as the remote description 89 | err = pc.SetRemoteDescription(desc) 90 | util.Check(err) 91 | } 92 | 93 | func (cp *ConnectionPair) createOfferer(config webrtc.Configuration) { 94 | // Create a new PeerConnection 95 | settingEngine := webrtc.SettingEngine{} 96 | settingEngine.SetICETimeouts(5*time.Second, 10*time.Second, 2*time.Second) 97 | api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) 98 | 99 | pc, err := api.NewPeerConnection(config) 100 | util.Check(err) 101 | 102 | buf := make([]byte, 1024) 103 | 104 | ordered := false 105 | maxRetransmits := uint16(0) 106 | hasSentData := false 107 | 108 | options := &webrtc.DataChannelInit{ 109 | Ordered: &ordered, 110 | MaxRetransmits: &maxRetransmits, 111 | } 112 | 113 | sendMoreCh := make(chan struct{}, 1) 114 | 115 | // Create a datachannel with label 'data' 116 | dc, err := pc.CreateDataChannel("data", options) 117 | util.Check(err) 118 | 119 | cp.OfferDC = dc 120 | 121 | if cp.iceServerInfo.Scheme == stun.SchemeTypeTURN || cp.iceServerInfo.Scheme == stun.SchemeTypeTURNS { 122 | 123 | // labels := map[string]string{ 124 | // "provider": cp.provider, 125 | // "scheme": cp.iceServerInfo.Scheme.String(), 126 | // "protocol": cp.iceServerInfo.Proto.String(), 127 | // "port": fmt.Sprintf("%d", cp.iceServerInfo.Port), 128 | // } 129 | 130 | // Register channel opening handling 131 | dc.OnOpen(func() { 132 | 133 | stats := pc.GetStats() 134 | iceTransportStats := stats["iceTransport"].(webrtc.TransportStats) 135 | // for k, v := range stats { 136 | cp.LogOfferer.Info("Offerer Stats", "iceTransportStats", iceTransportStats.BytesReceived) 137 | //} 138 | 139 | cp.LogOfferer.Info("OnOpen: Start sending a series of 1024-byte packets as fast as it can", "dataChannelLabel", dc.Label(), 140 | "dataChannelId", dc.ID(), 141 | ) 142 | 143 | for { 144 | if !hasSentData { 145 | cp.sentInitialMessageViaDC = time.Now() 146 | hasSentData = true 147 | } 148 | err2 := dc.Send(buf) 149 | if err2 != nil { 150 | break 151 | } 152 | 153 | if dc.BufferedAmount() > maxBufferedAmount { 154 | // Wait until the bufferedAmount becomes lower than the threshold 155 | <-sendMoreCh 156 | } 157 | } 158 | }) 159 | 160 | // Set bufferedAmountLowThreshold so that we can get notified when 161 | // we can send more 162 | dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) 163 | 164 | // This callback is made when the current bufferedAmount becomes lower than the threshold 165 | dc.OnBufferedAmountLow(func() { 166 | // Make sure to not block this channel or perform long running operations in this callback 167 | // This callback is executed by pion/sctp. If this callback is blocking it will stop operations 168 | if cp.doThroughputTest { 169 | select { 170 | case sendMoreCh <- struct{}{}: 171 | default: 172 | } 173 | } else { 174 | //a noop 175 | } 176 | }) 177 | 178 | dc.OnClose(func() { 179 | 180 | dcBytesSentTotal, _, iceTransportSentBytesTotal, iceTransportReceivedBytesTotal, _ := getBytesStats(pc, dc) 181 | 182 | cp.stats.SetOffererDcBytesSentTotal(float64(dcBytesSentTotal)) 183 | cp.stats.SetOffererIceTransportBytesSentTotal(float64(iceTransportSentBytesTotal)) 184 | cp.stats.SetOffererIceTransportBytesReceivedTotal(float64(iceTransportReceivedBytesTotal)) 185 | 186 | cp.LogOfferer.Info("Sent total", "dcSentBytesTotal", dcBytesSentTotal, 187 | "cpSentBytesTotal", iceTransportSentBytesTotal) 188 | }) 189 | } 190 | cp.OfferPC = pc 191 | } 192 | 193 | func (cp *ConnectionPair) createAnswerer(config webrtc.Configuration) { 194 | 195 | // settingEngine := webrtc.SettingEngine{} 196 | // settingEngine.SetICETimeouts(5, 5, 2) 197 | // api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) 198 | // Create a new PeerConnection 199 | pc, err := webrtc.NewPeerConnection(config) 200 | util.Check(err) 201 | 202 | if cp.iceServerInfo.Scheme == stun.SchemeTypeTURN || cp.iceServerInfo.Scheme == stun.SchemeTypeTURNS { 203 | 204 | // labels := map[string]string{ 205 | // "provider": cp.provider, 206 | // "scheme": cp.iceServerInfo.Scheme.String(), 207 | // "protocol": cp.iceServerInfo.Proto.String(), 208 | // "port": fmt.Sprintf("%d", cp.iceServerInfo.Port), 209 | // } 210 | 211 | pc.OnDataChannel(func(dc *webrtc.DataChannel) { 212 | var totalBytesReceived uint64 213 | 214 | hasReceivedData := false 215 | 216 | // Register channel opening handling 217 | dc.OnOpen(func() { 218 | 219 | cp.LogAnswerer.Info("OnOpen: Start receiving data", "dataChannelLabel", dc.Label(), 220 | "dataChannelId", dc.ID()) 221 | 222 | since := time.Now() 223 | 224 | lastTotalBytesReceived := uint64(0) 225 | // Start printing out the observed throughput 226 | for range time.NewTicker(100 * time.Millisecond).C { 227 | //check if this pc is closed and break out 228 | if pc.ConnectionState() != webrtc.PeerConnectionStateConnected { 229 | break 230 | } 231 | _, totalBytesReceivedTmp, _, _, ok := getBytesStats(pc, dc) 232 | if ok { 233 | totalBytesReceived = totalBytesReceivedTmp 234 | // cp.LogAnswerer.Info("Received Bytes So Far", "dcReceivedBytes", totalBytesReceivedTmp, 235 | // "cpReceivedBytes", cpTotalBytesReceivedTmp) 236 | } 237 | 238 | bytesLastTicker := totalBytesReceived - lastTotalBytesReceived 239 | 240 | bps := 8 * float64(bytesLastTicker) * 10 241 | lastTotalBytesReceived = totalBytesReceivedTmp 242 | 243 | averageBps := 8 * float64(totalBytesReceived) / float64(time.Since(since).Seconds()) 244 | // bps := float64(atomic.LoadUint64(&totalBytesReceived)*8) / time.Since(since).Seconds() 245 | cp.LogAnswerer.Info("On ticker: Calculated throughput", "bytesLastTicker", bytesLastTicker, "throughput", bps/1024/1024, "avgthroughput", averageBps/1024/1024, "eventTime", time.Now()) 246 | if cp.doThroughputTest { 247 | cp.stats.AddThroughput(time.Since(since).Milliseconds(), averageBps/1024/1024, bps/1024/1024) 248 | } 249 | } 250 | 251 | bps := 8 * float64(totalBytesReceived) / float64(time.Since(since).Seconds()) 252 | // bps := float64(atomic.LoadUint64(&totalBytesReceived)*8) / time.Since(since).Seconds() 253 | cp.LogAnswerer.Info("On ticker: Calculated throughput", "throughput", bps/1024/1024, 254 | "eventTime", time.Now(), 255 | "timeSinceStartMs", time.Since(since).Milliseconds()) 256 | if cp.doThroughputTest { 257 | cp.stats.AddThroughput(time.Since(since).Milliseconds(), bps/1024/1024, 0) 258 | } 259 | }) 260 | 261 | // Register the OnMessage to handle incoming messages 262 | dc.OnMessage(func(dcMsg webrtc.DataChannelMessage) { 263 | 264 | if !hasReceivedData { 265 | cp.stats.SetLatencyFirstPacket(float64(time.Since(cp.sentInitialMessageViaDC).Milliseconds())) 266 | cp.LogAnswerer.Info("Received first Packet", "latencyFirstPacketInMs", time.Since(cp.sentInitialMessageViaDC).Milliseconds()) 267 | hasReceivedData = true 268 | } 269 | if !cp.doThroughputTest { 270 | cp.LogAnswerer.Info("Sending to close") 271 | cp.closeChan <- struct{}{} 272 | } 273 | }) 274 | 275 | dc.OnClose(func() { 276 | 277 | _, dcBytesReceivedTotal, iceTransportBytesReceivedTotal, iceTransportBytesSentTotal, _ := getBytesStats(pc, dc) 278 | 279 | cp.stats.SetAnswererDcBytesReceivedTotal(float64(dcBytesReceivedTotal)) 280 | cp.stats.SetAnswererIceTransportBytesReceivedTotal(float64(iceTransportBytesReceivedTotal)) 281 | cp.stats.SetAnswererIceTransportBytesSentTotal(float64(iceTransportBytesSentTotal)) 282 | 283 | cp.LogAnswerer.Info("Received total", "dcReceivedBytesTotal", dcBytesReceivedTotal, 284 | "iceTransportReceivedBytesTotal", iceTransportBytesReceivedTotal) 285 | }) 286 | }) 287 | } 288 | 289 | cp.AnswerPC = pc 290 | } 291 | 292 | func getBytesStats(pc *webrtc.PeerConnection, dc *webrtc.DataChannel) (uint64, uint64, uint64, uint64, bool) { 293 | 294 | stats := pc.GetStats() 295 | 296 | // for _, report := range stats { 297 | // //if candidatePairStats, ok := report.(webrtc.ICECandidatePairStats); ok { 298 | // // Check if this candidate pair is the selected one 299 | // // if candidatePairStats.Nominated { 300 | // // fmt.Printf("WebRTC Stat: %+v\n", report) 301 | // // } 302 | // //} 303 | // } 304 | 305 | dcStats, ok := stats.GetDataChannelStats(dc) 306 | if !ok { 307 | return 0, 0, 0, 0, ok 308 | } 309 | 310 | iceTransportStats := stats["iceTransport"].(webrtc.TransportStats) 311 | 312 | return dcStats.BytesSent, dcStats.BytesReceived, iceTransportStats.BytesSent, iceTransportStats.BytesReceived, ok 313 | } 314 | -------------------------------------------------------------------------------- /client/ice-servers.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/nimbleape/iceperf-agent/adapters" 8 | "github.com/nimbleape/iceperf-agent/adapters/api" 9 | "github.com/nimbleape/iceperf-agent/adapters/cloudflare" 10 | "github.com/nimbleape/iceperf-agent/adapters/elixir" 11 | "github.com/nimbleape/iceperf-agent/adapters/expressturn" 12 | "github.com/nimbleape/iceperf-agent/adapters/google" 13 | "github.com/nimbleape/iceperf-agent/adapters/metered" 14 | "github.com/nimbleape/iceperf-agent/adapters/stunner" 15 | "github.com/nimbleape/iceperf-agent/adapters/twilio" 16 | "github.com/nimbleape/iceperf-agent/adapters/xirsys" 17 | "github.com/nimbleape/iceperf-agent/config" 18 | "github.com/pion/webrtc/v4" 19 | "github.com/rs/xid" 20 | // log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | func formGenericIceServers(config *config.ICEConfig) (adapters.IceServersConfig, error) { 24 | iceServers := []webrtc.ICEServer{} 25 | if config.StunEnabled { 26 | for proto, ports := range config.StunPorts { 27 | query := "" 28 | if !config.StunUseRFC7094URI { 29 | query = fmt.Sprintf("?transport=%s", proto) 30 | } 31 | for _, port := range ports { 32 | stunProto := "stun" 33 | if proto == "tls" { 34 | stunProto = "stuns" 35 | } 36 | url := fmt.Sprintf("%s:%s:%d%s", stunProto, config.StunHost, port, query) 37 | 38 | iceServers = append(iceServers, 39 | webrtc.ICEServer{ 40 | URLs: []string{url}, 41 | Username: config.Username, 42 | Credential: config.Password, 43 | CredentialType: webrtc.ICECredentialTypePassword, 44 | }) 45 | 46 | } 47 | } 48 | } 49 | if config.TurnEnabled { 50 | for proto, ports := range config.TurnPorts { 51 | for _, port := range ports { 52 | turnProto := "turn" 53 | l4proto := proto 54 | if proto == "tls" { 55 | turnProto = "turns" 56 | l4proto = "tcp" 57 | } 58 | if proto == "dtls" { 59 | turnProto = "turns" 60 | l4proto = "udp" 61 | } 62 | url := fmt.Sprintf("%s:%s:%d?transport=%s", 63 | turnProto, config.TurnHost, port, l4proto) 64 | 65 | iceServers = append(iceServers, 66 | webrtc.ICEServer{ 67 | URLs: []string{url}, 68 | Username: config.Username, 69 | Credential: config.Password, 70 | CredentialType: webrtc.ICECredentialTypePassword, 71 | }) 72 | } 73 | } 74 | 75 | } 76 | c := adapters.IceServersConfig{ 77 | IceServers: iceServers, 78 | DoThroughput: config.DoThroughput, 79 | } 80 | 81 | return c, nil 82 | } 83 | 84 | type IceServersConfig struct { 85 | IceServers map[string][]webrtc.ICEServer 86 | DoThroughput bool 87 | } 88 | 89 | func GetIceServers(config *config.Config, logger *slog.Logger, testRunId xid.ID) (map[string]adapters.IceServersConfig, string, error) { 90 | 91 | //check if the API is set and is enabled 92 | if apiConfig, ok := config.ICEConfig["api"]; ok && apiConfig.Enabled { 93 | md := api.Driver{ 94 | Config: &apiConfig, 95 | Logger: logger, 96 | } 97 | iceServers, node, err := md.GetIceServers(testRunId) 98 | return iceServers, node, err 99 | } 100 | 101 | iceServers := make(map[string]adapters.IceServersConfig) 102 | 103 | //loop through 104 | for key, conf := range config.ICEConfig { 105 | switch key { 106 | case "api": 107 | continue 108 | case "elixir": 109 | if !conf.Enabled { 110 | continue 111 | } 112 | md := elixir.Driver{ 113 | Config: &conf, 114 | Logger: logger, 115 | } 116 | is, err := md.GetIceServers() 117 | if err != nil { 118 | logger.Error("Error getting elixir ice servers") 119 | return nil, "", err 120 | } 121 | logger.Info("elixir IceServers", "is", is) 122 | iceServers[key] = is 123 | case "google": 124 | if !conf.Enabled { 125 | continue 126 | } 127 | md := google.Driver{ 128 | Config: &conf, 129 | Logger: logger, 130 | } 131 | is, err := md.GetIceServers() 132 | if err != nil { 133 | logger.Error("Error getting google ice servers") 134 | return nil, "", err 135 | } 136 | logger.Info("google IceServers", "is", is) 137 | 138 | iceServers[key] = is 139 | case "metered": 140 | if !conf.Enabled { 141 | continue 142 | } 143 | md := metered.Driver{ 144 | Config: &conf, 145 | Logger: logger, 146 | } 147 | is, err := md.GetIceServers() 148 | if err != nil { 149 | logger.Error("Error getting metered ice servers") 150 | return nil, "", err 151 | } 152 | logger.Info("metered IceServers", "is", is) 153 | 154 | iceServers[key] = is 155 | case "stunner": 156 | if !conf.Enabled { 157 | continue 158 | } 159 | md := stunner.Driver{ 160 | Config: &conf, 161 | Logger: logger, 162 | } 163 | is, err := md.GetIceServers() 164 | if err != nil { 165 | logger.Error("Error getting elixir ice servers") 166 | return nil, "", err 167 | } 168 | logger.Info("STUNner IceServers", "is", is) 169 | iceServers[key] = is 170 | case "twilio": 171 | if !conf.Enabled { 172 | continue 173 | } 174 | td := twilio.Driver{ 175 | Config: &conf, 176 | Logger: logger, 177 | } 178 | is, err := td.GetIceServers() 179 | if err != nil { 180 | logger.Error("Error getting twilio ice servers") 181 | return nil, "", err 182 | } 183 | logger.Info("twilio IceServers", "is", is) 184 | 185 | iceServers[key] = is 186 | case "xirsys": 187 | if !conf.Enabled { 188 | continue 189 | } 190 | xd := xirsys.Driver{ 191 | Config: &conf, 192 | Logger: logger, 193 | } 194 | is, err := xd.GetIceServers() 195 | if err != nil { 196 | logger.Error("Error getting xirsys ice servers") 197 | return nil, "", err 198 | } 199 | logger.Info("xirsys IceServers", "is", is) 200 | 201 | iceServers[key] = is 202 | case "cloudflare": 203 | if !conf.Enabled { 204 | continue 205 | } 206 | cd := cloudflare.Driver{ 207 | Config: &conf, 208 | Logger: logger, 209 | } 210 | is, err := cd.GetIceServers() 211 | if err != nil { 212 | logger.Error("Error getting cloudflare ice servers") 213 | return nil, "", err 214 | } 215 | logger.Info("cloudflare IceServers", "is", is) 216 | 217 | iceServers[key] = is 218 | case "expressturn": 219 | if !conf.Enabled { 220 | continue 221 | } 222 | ed := expressturn.Driver{ 223 | Config: &conf, 224 | Logger: logger, 225 | } 226 | is, err := ed.GetIceServers() 227 | if err != nil { 228 | logger.Error("Error getting expressturn ice servers") 229 | 230 | return nil, "", err 231 | } 232 | logger.Info("expressturn IceServers", "is", is) 233 | 234 | iceServers[key] = is 235 | default: 236 | is, err := formGenericIceServers(&conf) 237 | if err != nil { 238 | logger.Error("Error getting generic ice servers") 239 | return nil, "", err 240 | } 241 | logger.Info("default IceServers", "key", key, "is", is) 242 | 243 | iceServers[key] = is 244 | } 245 | } 246 | 247 | return iceServers, "", nil 248 | } 249 | -------------------------------------------------------------------------------- /cmd/iceperf/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "os" 11 | "time" 12 | 13 | "github.com/nimbleape/iceperf-agent/client" 14 | "github.com/nimbleape/iceperf-agent/config" 15 | "github.com/nimbleape/iceperf-agent/stats" 16 | "github.com/nimbleape/iceperf-agent/version" 17 | "github.com/pion/stun/v2" 18 | "github.com/pion/webrtc/v4" 19 | "github.com/prometheus/client_golang/prometheus" 20 | 21 | // "github.com/prometheus/client_golang/prometheus/push" 22 | 23 | "github.com/rs/xid" 24 | 25 | // slogloki "github.com/samber/slog-loki/v3" 26 | 27 | slogmulti "github.com/samber/slog-multi" 28 | "github.com/urfave/cli/v2" 29 | 30 | // "github.com/grafana/loki-client-go/loki" 31 | loki "github.com/magnetde/slog-loki" 32 | 33 | "github.com/fatih/color" 34 | "github.com/rodaine/table" 35 | ) 36 | 37 | func main() { 38 | app := &cli.App{ 39 | Name: "ICEPerf", 40 | Usage: "ICE Servers performance tests", 41 | Version: version.Version, 42 | Description: "Run ICE Servers performance tests and report results", 43 | Flags: []cli.Flag{ 44 | &cli.StringFlag{ 45 | Name: "config", 46 | Aliases: []string{"c"}, 47 | Usage: "ICEPerf yaml config file", 48 | }, 49 | &cli.StringFlag{ 50 | Name: "api-uri", 51 | Aliases: []string{"a"}, 52 | Usage: "API URI", 53 | }, 54 | &cli.StringFlag{ 55 | Name: "api-key", 56 | Aliases: []string{"k"}, 57 | Usage: "API Key", 58 | }, 59 | &cli.BoolFlag{ 60 | Name: "timer", 61 | Aliases: []string{"t"}, 62 | Value: false, 63 | Usage: "Enable Timer Mode", 64 | }, 65 | }, 66 | Action: runService, 67 | } 68 | 69 | if err := app.Run(os.Args); err != nil { 70 | fmt.Println(err) 71 | } 72 | } 73 | 74 | func runService(ctx *cli.Context) error { 75 | config, err := getConfig(ctx) 76 | if err != nil { 77 | fmt.Println("Error loading config") 78 | return err 79 | } 80 | 81 | lvl := new(slog.LevelVar) 82 | lvl.Set(slog.LevelError) 83 | 84 | // Configure the logger 85 | 86 | var logg *slog.Logger 87 | 88 | var loggingLevel slog.Level 89 | 90 | switch config.Logging.Level { 91 | case "debug": 92 | loggingLevel = slog.LevelDebug 93 | case "info": 94 | loggingLevel = slog.LevelInfo 95 | case "error": 96 | loggingLevel = slog.LevelError 97 | default: 98 | loggingLevel = slog.LevelInfo 99 | } 100 | 101 | if config.Logging.Loki.Enabled { 102 | 103 | // config, _ := loki.NewDefaultConfig(config.Logging.Loki.URL) 104 | // // config.TenantID = "xyz" 105 | // client, _ := loki.New(config) 106 | 107 | lokiHandler := loki.NewHandler( 108 | config.Logging.Loki.URL, 109 | loki.WithLabelsEnabled(loki.LabelAll...), 110 | loki.WithHandler(func(w io.Writer) slog.Handler { 111 | return slog.NewJSONHandler(w, &slog.HandlerOptions{ 112 | Level: loggingLevel, 113 | }) 114 | })) 115 | 116 | logg = slog.New( 117 | slogmulti.Fanout( 118 | slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 119 | Level: loggingLevel, 120 | }), 121 | // slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ 122 | // Level: slog.LevelInfo, 123 | // }), 124 | lokiHandler, 125 | ), 126 | ).With("app", "iceperf") 127 | 128 | // stop loki client and purge buffers 129 | defer lokiHandler.Close() 130 | 131 | // opts := lokirus.NewLokiHookOptions(). 132 | // // Grafana doesn't have a "panic" level, but it does have a "critical" level 133 | // // https://grafana.com/docs/grafana/latest/explore/logs-integration/ 134 | // WithLevelMap(lokirus.LevelMap{log.PanicLevel: "critical"}). 135 | // WithFormatter(&logrus.JSONFormatter{}). 136 | // WithStaticLabels(lokirus.Labels{ 137 | // "app": "iceperftest", 138 | // }) 139 | 140 | // if config.Logging.Loki.UseBasicAuth { 141 | // opts.WithBasicAuth(config.Logging.Loki.Username, config.Logging.Loki.Password) 142 | // } 143 | 144 | // if config.Logging.Loki.UseHeadersAuth { 145 | // httpClient := &http.Client{Transport: &transport{underlyingTransport: http.DefaultTransport, authHeaders: config.Logging.Loki.AuthHeaders}} 146 | 147 | // opts.WithHttpClient(httpClient) 148 | // } 149 | 150 | // hook := lokirus.NewLokiHookWithOpts( 151 | // config.Logging.Loki.URL, 152 | // opts, 153 | // log.InfoLevel, 154 | // log.WarnLevel, 155 | // log.ErrorLevel, 156 | // log.FatalLevel) 157 | 158 | // logg.AddHook(hook) 159 | 160 | // lokiHookConfig := &lokihook.Config{ 161 | // // the loki api url 162 | // URL: config.Logging.Loki.URL, 163 | // // (optional, default: severity) the label's key to distinguish log's level, it will be added to Labels map 164 | // LevelName: "level", 165 | // // the labels which will be sent to loki, contains the {levelname: level} 166 | // Labels: map[string]string{ 167 | // "app": "iceperftest", 168 | // }, 169 | // } 170 | // hook, err := lokihook.NewHook(lokiHookConfig) 171 | // if err != nil { 172 | // log.Error(err) 173 | // } else { 174 | // log.AddHook(hook) 175 | // } 176 | 177 | // hook := loki.NewHook(config.Logging.Loki.URL, loki.WithLabel("app", "iceperftest"), loki.WithFormatter(&logrus.JSONFormatter{}), loki.WithLevel(log.InfoLevel)) 178 | // defer hook.Close() 179 | 180 | // log.AddHook(hook) 181 | } else { 182 | handlerOpts := &slog.HandlerOptions{ 183 | Level: loggingLevel, 184 | } 185 | logg = slog.New(slog.NewTextHandler(os.Stderr, handlerOpts)) 186 | } 187 | slog.SetDefault(logg) 188 | 189 | if config.Timer.Enabled { 190 | ticker := time.NewTicker(time.Duration(config.Timer.Interval) * time.Minute) 191 | runTest(logg, config) 192 | for { 193 | <-ticker.C 194 | runTest(logg, config) 195 | } 196 | } else { 197 | runTest(logg, config) 198 | } 199 | 200 | return nil 201 | } 202 | 203 | func runTest(logg *slog.Logger, config *config.Config) error { 204 | // logg.SetFormatter(&log.JSONFormatter{PrettyPrint: true}) 205 | 206 | testRunId := xid.New() 207 | testRunStartedAt := time.Now() 208 | 209 | logger := logg.With("testRunId", testRunId) 210 | 211 | // TODO we will make a new client for each ICE Server URL from each provider 212 | // get ICE servers and loop them 213 | ICEServers, node, err := client.GetIceServers(config, logger, testRunId) 214 | if err != nil { 215 | logger.Error("Error getting ICE servers", "err", err) 216 | //this should be a fatal 217 | } 218 | 219 | if node != "" { 220 | config.NodeID = node 221 | } 222 | 223 | config.Registry = prometheus.NewRegistry() 224 | // pusher := push.New(config.Logging.Loki.URL, "grafanacloud-nimbleape-prom").Gatherer(config.Registry) 225 | // pusher := push.New() 226 | // pusher.Gatherer(config.Registry) 227 | // promClient := promwrite.NewClient(config.Logging.Prometheus.URL) 228 | 229 | // TEST writing to qryn 230 | // if _, err := promClient.Write( 231 | // ctx.Context, 232 | // &promwrite.WriteRequest{ 233 | // TimeSeries: []promwrite.TimeSeries{ 234 | // { 235 | // Labels: []promwrite.Label{ 236 | // { 237 | // Name: "__name__", 238 | // Value: "test_metric", 239 | // }, 240 | // }, 241 | // Sample: promwrite.Sample{ 242 | // Time: time.Now(), 243 | // Value: 123, 244 | // }, 245 | // }, 246 | // }, 247 | // }, 248 | // promwrite.WriteHeaders(config.Logging.Prometheus.AuthHeaders), 249 | // ); err != nil { 250 | // logger.Error("Error writing to Qryn", err) 251 | // } 252 | // end TEST 253 | 254 | var results []*stats.Stats 255 | 256 | for provider, iss := range ICEServers { 257 | providerLogger := logger.With("Provider", provider) 258 | 259 | providerLogger.Info("Provider Starting") 260 | 261 | for _, is := range iss.IceServers { 262 | 263 | providerLogger.Info("URL is", "url", is) 264 | 265 | iceServerInfo, err := stun.ParseURI(is.URLs[0]) 266 | 267 | if err != nil { 268 | providerLogger.Error("Error parsing ICE Server URL", "err", err) 269 | continue 270 | } 271 | 272 | runId := xid.New() 273 | 274 | iceServerLogger := providerLogger.With("iceServerTestRunId", runId, 275 | "schemeAndProtocol", iceServerInfo.Scheme.String()+"-"+iceServerInfo.Proto.String(), 276 | ) 277 | 278 | iceServerLogger.Info("Starting New Client", "iceServerHost", iceServerInfo.Host, 279 | "iceServerProtocol", iceServerInfo.Proto.String(), 280 | "iceServerPort", iceServerInfo.Port, 281 | "iceServerScheme", iceServerInfo.Scheme.String(), 282 | ) 283 | config.Logger = iceServerLogger 284 | 285 | config.WebRTCConfig.ICEServers = []webrtc.ICEServer{is} 286 | //if the ice server is a stun then set the 287 | testDuration := 20 * time.Second 288 | if iceServerInfo.Scheme == stun.SchemeTypeSTUN || iceServerInfo.Scheme == stun.SchemeTypeSTUNS { 289 | config.WebRTCConfig.ICETransportPolicy = webrtc.ICETransportPolicyAll 290 | testDuration = 2 * time.Second 291 | } else { 292 | config.WebRTCConfig.ICETransportPolicy = webrtc.ICETransportPolicyRelay 293 | } 294 | 295 | timer := time.NewTimer(testDuration) 296 | close := make(chan struct{}) 297 | 298 | c, err := client.NewClient(config, iceServerInfo, provider, testRunId, testRunStartedAt, iss.DoThroughput, close) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | iceServerLogger.Info("Calling Run()") 304 | c.Run() 305 | iceServerLogger.Info("Called Run(), waiting for timer", "seconds", testDuration.Seconds()) 306 | select { 307 | case <-close: 308 | timer.Stop() 309 | case <-timer.C: 310 | } 311 | iceServerLogger.Info("Calling Stop()") 312 | c.Stop() 313 | <-time.After(1 * time.Second) 314 | iceServerLogger.Info("Finished") 315 | results = append(results, c.Stats) 316 | } 317 | providerLogger.Info("Provider Finished") 318 | } 319 | 320 | logger.Info("Finished Test Run") 321 | 322 | // c, err := client.NewClient(config) 323 | // if err != nil { 324 | // return nil 325 | // } 326 | // defer c.Stop() 327 | 328 | // c.Run() 329 | 330 | // util.Check(pusher.Push(config.Logging.Prometheus.URL)) 331 | 332 | // write all metrics to qryn at once 333 | // mf, err := config.Registry.Gather() 334 | // if err != nil { 335 | // logger.Error("Error gathering metrics from registry", err) 336 | // } 337 | 338 | // if len(mf) > 0 { 339 | 340 | // timenow := time.Now() 341 | 342 | // ts := []promwrite.TimeSeries{} 343 | // for _, m := range mf { 344 | 345 | // //loop thorugh each metric 346 | // for _, met := range m.GetMetric() { 347 | // var v float64 348 | // switch m.GetType().String() { 349 | // case "GAUGE": 350 | // v = *met.Gauge.Value 351 | // //add more 352 | // } 353 | 354 | // labels := []promwrite.Label{ 355 | // { 356 | // Name: "__name__", 357 | // Value: m.GetName(), 358 | // }, 359 | // { 360 | // Name: "description", 361 | // Value: m.GetHelp(), 362 | // }, 363 | // { 364 | // Name: "type", 365 | // Value: m.GetType().String(), 366 | // }, 367 | // } 368 | 369 | // for _, lp := range met.GetLabel() { 370 | // labels = append(labels, promwrite.Label{Name: *lp.Name, Value: *lp.Value}) 371 | // } 372 | 373 | // ts = append(ts, promwrite.TimeSeries{ 374 | // Labels: labels, 375 | // Sample: promwrite.Sample{ 376 | // Time: timenow, 377 | // Value: v, 378 | // }, 379 | // }) 380 | 381 | // logger.Info("got metrics", "labels", met.Label, "name", m.GetName(), "type", m.GetType(), "value", v, "unit", m.GetUnit(), "description", m.GetHelp()) 382 | // } 383 | // } 384 | // _, err := promClient.Write( 385 | // ctx.Context, 386 | // &promwrite.WriteRequest{ 387 | // TimeSeries: ts, 388 | // }, 389 | // promwrite.WriteHeaders(config.Logging.Prometheus.AuthHeaders), 390 | // ) 391 | // if err != nil { 392 | // logger.Error("Error writing to Qryn", err) 393 | // } 394 | // logger.Info("Wrote stats to prom") 395 | 396 | // } 397 | // if !config.Logging.Loki.Enabled && !config.Logging.API.Enabled { 398 | headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() 399 | columnFmt := color.New(color.FgYellow).SprintfFunc() 400 | 401 | tbl := table.New("Provider", "Scheme", "Protocol", "Time to candidate", "Time to Connected State", "Max Throughput", "TURN Transfer Latency") 402 | tbl.WithHeaderFormatter(headerFmt).WithFirstColumnFormatter(columnFmt) 403 | 404 | for _, st := range results { 405 | tbl.AddRow(st.Provider, st.Scheme, st.Protocol, st.OffererTimeToReceiveCandidate, st.TimeToConnectedState, st.ThroughputMax, st.LatencyFirstPacket) 406 | } 407 | 408 | tbl.Print() 409 | //} 410 | return nil 411 | } 412 | 413 | func getConfig(c *cli.Context) (*config.Config, error) { 414 | configBody := "" 415 | configFile := c.String("config") 416 | if configFile != "" { 417 | content, err := os.ReadFile(configFile) 418 | if err != nil { 419 | return nil, err 420 | } 421 | configBody = string(content) 422 | } 423 | 424 | conf, err := config.NewConfig(configBody) 425 | if err != nil { 426 | return nil, err 427 | } 428 | 429 | //if we got passed in the api host and the api key then overwrite the config 430 | //same for timer mode 431 | if c.String("api-uri") != "" { 432 | conf.Api.Enabled = true 433 | conf.Api.URI = c.String("api-uri") 434 | } 435 | 436 | if c.String("api-key") != "" { 437 | conf.Api.Enabled = true 438 | conf.Api.ApiKey = c.String("api-key") 439 | } 440 | 441 | if conf.Api.Enabled && conf.Api.URI == "" { 442 | conf.Api.URI = "https://api.iceperf.com/api/settings" 443 | } 444 | 445 | if c.Bool("timer") { 446 | conf.Timer.Enabled = true 447 | conf.Timer.Interval = 60 448 | } 449 | 450 | if conf.Api.Enabled && conf.Api.ApiKey != "" && conf.Api.URI != "" { 451 | conf.UpdateConfigFromApi() 452 | } 453 | 454 | return conf, nil 455 | } 456 | 457 | // func setLogLevel(logger *log.Logger, level string) { 458 | // switch level { 459 | // case "debug": 460 | // logger.SetLevel(slog.DebugLevel) 461 | // case "error": 462 | // logger.SetLevel(slog.ErrorLevel) 463 | // case "fatal": 464 | // logger.SetLevel(slog.FatalLevel) 465 | // case "panic": 466 | // logger.SetLevel(slog.PanicLevel) 467 | // case "trace": 468 | // logger.SetLevel(slog.TraceLevel) 469 | // case "warn": 470 | // logger.SetLevel(slog.WarnLevel) 471 | // default: 472 | // logger.SetLevel(slog.InfoLevel) 473 | // } 474 | // } 475 | -------------------------------------------------------------------------------- /config-api.yaml.example: -------------------------------------------------------------------------------- 1 | timer: 2 | enabled: true 3 | interval: 60 4 | api: 5 | enabled: true 6 | uri: https://api.iceperf.com/api/settings 7 | api_key: your-api-key 8 | -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | node_id: 1 2 | timer: 3 | enabled: true 4 | interval: 60 5 | logging: 6 | level: info 7 | api: 8 | enabled: true 9 | uri: https://api.iceperf.com/api/insert 10 | api_key: your-api-key 11 | loki: 12 | enabled: false 13 | url: a-loki-push-url 14 | ice_servers: 15 | api: 16 | enabled: false 17 | request_url: https://api.iceperf.com/api/iceServers 18 | api_key: your-api-key 19 | metered: 20 | enabled: true 21 | api_key: your-metered-api-key 22 | request_url: https://your-subdomain.metered.live/api/v1/turn/credentials 23 | stun_enabled: false 24 | turn_enabled: false 25 | do_throughput: false 26 | cloudflare: 27 | enabled: true 28 | request_url: https://rtc.live.cloudflare.com/v1/turn/keys/your-app-id/credentials/generate 29 | api_key: your-cloudflare-api-key 30 | stun_enabled: true 31 | turn_enabled: true 32 | do_throughput: false 33 | twilio: 34 | enabled: false 35 | http_username: your-twilio-account-id 36 | http_password: your-account-secret 37 | request_url: https://api.twilio.com/2010-04-01/Accounts/your-twilio-account-id/Tokens.json 38 | stun_enabled: false 39 | turn_enabled: false 40 | do_throughput: false 41 | google: 42 | enabled: false 43 | stun_host: stun.l.google.com 44 | stun_enabled: false 45 | turn_enabled: false 46 | stun_ports: 47 | udp: 48 | - 19302 49 | xirsys: 50 | enabled: false 51 | http_username: your-xirsys-username 52 | http_password: your-xirsys-api-password 53 | request_url: https://global.xirsys.net/_turn/your-app-id 54 | stun_enabled: false 55 | turn_enabled: false 56 | do_throughput: false 57 | expressturn: 58 | enabled: false 59 | username: expressturn-cred-username 60 | password: expressturn-cred-password 61 | stun_host: relay1.expressturn.com 62 | turn_host: relay1.expressturn.com 63 | stun_enabled: false 64 | turn_enabled: false 65 | do_throughput: false 66 | stun_ports: 67 | udp: 68 | - 3478 69 | # - 53 70 | turn_ports: 71 | udp: 72 | - 3478 73 | # - 80 74 | tcp: 75 | - 3478 76 | # - 443 77 | tls: 78 | - 5349 79 | # - 443 -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "reflect" 10 | 11 | "github.com/pion/webrtc/v4" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | type ICEConfig struct { 17 | Username string `yaml:"username,omitempty"` 18 | Password string `yaml:"password,omitempty"` 19 | ApiKey string `json:"apiKey,omitempty" yaml:"api_key,omitempty"` 20 | AccountSid string `yaml:"account_sid,omitempty"` 21 | RequestUrl string `json:"requestUrl,omitempty" yaml:"request_url,omitempty"` 22 | HttpUsername string `yaml:"http_username"` 23 | HttpPassword string `yaml:"http_password"` 24 | Enabled bool `yaml:"enabled"` 25 | StunUseRFC7094URI bool `yaml:"stun_use_rfc7094_uri"` 26 | StunHost string `yaml:"stun_host,omitempty"` 27 | TurnHost string `yaml:"turn_host,omitempty"` 28 | TurnPorts map[string][]int `yaml:"turn_ports,omitempty"` 29 | StunPorts map[string][]int `yaml:"stun_ports,omitempty"` 30 | StunEnabled bool `yaml:"stun_enabled"` 31 | TurnEnabled bool `yaml:"turn_enabled"` 32 | DoThroughput bool `yaml:"do_throughput"` 33 | } 34 | 35 | type LokiConfig struct { 36 | Enabled bool `json:"enabled" yaml:"enabled"` 37 | UseBasicAuth bool `yaml:"use_basic_auth"` 38 | UseHeadersAuth bool `yaml:"use_headers_auth"` 39 | Username string `yaml:"username,omitempty"` 40 | Password string `yaml:"password,omitempty"` 41 | URL string `json:"url" yaml:"url"` 42 | AuthHeaders map[string]string `yaml:"auth_headers,omitempty"` 43 | } 44 | 45 | type PromConfig struct { 46 | Enabled bool `yaml:"enabled"` 47 | URL string `yaml:"url"` 48 | AuthHeaders map[string]string `yaml:"auth_headers,omitempty"` 49 | } 50 | 51 | type ApiConfig struct { 52 | Enabled bool `json:"enabled" yaml:"enabled"` 53 | URI string `json:"uri" yaml:"uri"` 54 | ApiKey string `json:"apiKey,omitempty" yaml:"api_key,omitempty"` 55 | } 56 | 57 | type LoggingConfig struct { 58 | Level string `yaml:"level"` 59 | API ApiConfig `json:"api" yaml:"api"` 60 | Loki LokiConfig `json:"loki" yaml:"loki"` 61 | Prometheus PromConfig `yaml:"prometheus"` 62 | } 63 | 64 | type TimerConfig struct { 65 | Enabled bool `json:"enabled" yaml:"enabled"` 66 | Interval int `json:"interval" yaml:"interval"` 67 | } 68 | 69 | type Config struct { 70 | NodeID string `json:"nodeId" yaml:"node_id"` 71 | ICEConfig map[string]ICEConfig `json:"iceServers" yaml:"ice_servers"` 72 | Logging LoggingConfig `json:"logging" yaml:"logging"` 73 | Timer TimerConfig `json:"timer" yaml:"timer"` 74 | Api ApiConfig `json:"api" yaml:"api"` 75 | 76 | WebRTCConfig webrtc.Configuration 77 | // TODO the following should be different for answerer and offerer sides 78 | OnICECandidate func(*webrtc.ICECandidate) 79 | OnConnectionStateChange func(s webrtc.PeerConnectionState) 80 | 81 | // internal 82 | ServiceName string `yaml:"-"` 83 | Logger *slog.Logger 84 | Registry *prometheus.Registry 85 | } 86 | 87 | func mergeConfigs(c, responseConfig interface{}) { 88 | mergeStructs(reflect.ValueOf(c).Elem(), reflect.ValueOf(responseConfig).Elem()) 89 | } 90 | 91 | func mergeStructs(cValue, respValue reflect.Value) { 92 | for i := 0; i < respValue.NumField(); i++ { 93 | respField := respValue.Field(i) 94 | cField := cValue.Field(i) 95 | 96 | if !respField.IsZero() { 97 | switch respField.Kind() { 98 | case reflect.Ptr: 99 | if !respField.IsNil() { 100 | if cField.IsNil() { 101 | cField.Set(reflect.New(cField.Type().Elem())) 102 | } 103 | mergeStructs(cField.Elem(), respField.Elem()) 104 | } 105 | case reflect.Struct: 106 | mergeStructs(cField, respField) 107 | case reflect.Map: 108 | if cField.IsNil() { 109 | cField.Set(reflect.MakeMap(cField.Type())) 110 | } 111 | for _, key := range respField.MapKeys() { 112 | val := respField.MapIndex(key) 113 | cField.SetMapIndex(key, val) 114 | } 115 | default: 116 | cField.Set(respField) 117 | } 118 | } 119 | } 120 | } 121 | 122 | func NewConfig(confString string) (*Config, error) { 123 | c := &Config{ 124 | ServiceName: "ICEPerf", 125 | } 126 | if confString != "" { 127 | if err := yaml.Unmarshal([]byte(confString), c); err != nil { 128 | return nil, err 129 | } 130 | } 131 | return c, nil 132 | } 133 | 134 | func (c *Config) UpdateConfigFromApi() error { 135 | httpClient := &http.Client{} 136 | 137 | req, err := http.NewRequest("GET", c.Api.URI, nil) 138 | req.Header.Add("Content-Type", "application/json") 139 | req.Header.Add("Authorization", "Bearer "+c.Api.ApiKey) 140 | 141 | if err != nil { 142 | return err 143 | } 144 | 145 | res, err := httpClient.Do(req) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | defer res.Body.Close() 151 | //check the code of the response 152 | if res.StatusCode != 200 { 153 | err = errors.New("error from our api " + res.Status) 154 | return err 155 | } 156 | 157 | responseData, err := io.ReadAll(res.Body) 158 | if err != nil { 159 | return err 160 | } 161 | responseConfig := Config{} 162 | json.Unmarshal([]byte(responseData), &responseConfig) 163 | 164 | //go and merge in values from the API into the config 165 | 166 | //lets just do the basics for now.... 167 | //this needs a lot more work 168 | c.NodeID = responseConfig.NodeID 169 | c.ICEConfig = responseConfig.ICEConfig 170 | c.Logging = responseConfig.Logging 171 | // mergeConfigs(c, responseConfig) 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nimbleape/iceperf-agent 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/alecthomas/assert/v2 v2.5.0 7 | github.com/fatih/color v1.17.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/magnetde/slog-loki v0.1.4 10 | github.com/pion/stun/v2 v2.0.0 11 | github.com/pion/webrtc/v4 v4.0.0-beta.29 12 | github.com/prometheus/client_golang v1.11.1 13 | github.com/rodaine/table v1.2.0 14 | github.com/rs/xid v1.5.0 15 | github.com/samber/slog-multi v1.0.3 16 | github.com/urfave/cli/v2 v2.27.1 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/alecthomas/repr v0.3.0 // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 24 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 25 | github.com/golang/protobuf v1.5.3 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/hashicorp/errwrap v1.1.0 // indirect 28 | github.com/hashicorp/go-multierror v1.1.1 // indirect 29 | github.com/hexops/gotextdiff v1.0.3 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 33 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 34 | github.com/pion/datachannel v1.5.9 // indirect 35 | github.com/pion/dtls/v2 v2.2.12 // indirect 36 | github.com/pion/dtls/v3 v3.0.1 // indirect 37 | github.com/pion/ice/v3 v3.0.16 // indirect 38 | github.com/pion/ice/v4 v4.0.1 // indirect 39 | github.com/pion/interceptor v0.1.30 // indirect 40 | github.com/pion/logging v0.2.2 // indirect 41 | github.com/pion/mdns/v2 v2.0.7 // indirect 42 | github.com/pion/randutil v0.1.0 // indirect 43 | github.com/pion/rtcp v1.2.14 // indirect 44 | github.com/pion/rtp v1.8.9 // indirect 45 | github.com/pion/sctp v1.8.33 // indirect 46 | github.com/pion/sdp/v3 v3.0.9 // indirect 47 | github.com/pion/srtp/v3 v3.0.3 // indirect 48 | github.com/pion/stun/v3 v3.0.0 // indirect 49 | github.com/pion/transport/v2 v2.2.8 // indirect 50 | github.com/pion/transport/v3 v3.0.7 // indirect 51 | github.com/pion/turn/v3 v3.0.3 // indirect 52 | github.com/pion/turn/v4 v4.0.0 // indirect 53 | github.com/prometheus/client_model v0.6.1 // indirect 54 | github.com/prometheus/common v0.30.0 // indirect 55 | github.com/prometheus/procfs v0.12.0 // indirect 56 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 57 | github.com/samber/lo v1.38.1 // indirect 58 | github.com/wlynxg/anet v0.0.3 // indirect 59 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 60 | golang.org/x/crypto v0.26.0 // indirect 61 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect 62 | golang.org/x/net v0.27.0 // indirect 63 | golang.org/x/sys v0.24.0 // indirect 64 | google.golang.org/protobuf v1.34.0 // indirect 65 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w= 37 | github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= 38 | github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= 39 | github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 40 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 41 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 42 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 43 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 44 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 45 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 46 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 47 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 48 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 49 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 50 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 52 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 53 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 54 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 55 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 56 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 57 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 58 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 59 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 60 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 61 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 62 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 64 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 65 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 66 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 67 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 68 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 69 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 70 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 71 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 72 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 73 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 74 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 75 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 76 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 77 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 78 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 79 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 83 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 86 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 87 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 88 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 89 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 90 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 91 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 94 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 95 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 96 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 97 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 98 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 99 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 100 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 101 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 102 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 103 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 104 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 105 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 106 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 107 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 108 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 109 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 110 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 111 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 112 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 113 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 117 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 118 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 119 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 120 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 121 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 122 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 123 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 124 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 125 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 126 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 127 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 128 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 129 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 130 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 131 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 132 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 133 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 134 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 135 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 136 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 137 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 138 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 139 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 140 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 141 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 142 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 143 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 144 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 145 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 146 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 147 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 148 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 149 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 150 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 151 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 152 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 153 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 154 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 155 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 156 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 157 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 158 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 159 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 160 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 161 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 162 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 163 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 164 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 165 | github.com/magnetde/slog-loki v0.1.4 h1:AEWQffyDU7ZPGPT5peupffD341c8WUBK+daCOg1JaEk= 166 | github.com/magnetde/slog-loki v0.1.4/go.mod h1:1XxjKs1ndcTZujCF8eEeScqV5JLVYNzR6JvSsllrkO4= 167 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 168 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 169 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 170 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 171 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 172 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 173 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 174 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 175 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 176 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 177 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 178 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 179 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 180 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 181 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 182 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 183 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 184 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 185 | github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo= 186 | github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI= 187 | github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= 188 | github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= 189 | github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= 190 | github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= 191 | github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= 192 | github.com/pion/dtls/v3 v3.0.0 h1:m2hzwPkzqoBjVKXm5ymNuX01OAjht82TdFL6LoTzgi4= 193 | github.com/pion/dtls/v3 v3.0.0/go.mod h1:tiX7NaneB0wNoRaUpaMVP7igAlkMCTQkbpiY+OfeIi0= 194 | github.com/pion/dtls/v3 v3.0.1 h1:0kmoaPYLAo0md/VemjcrAXQiSf8U+tuU3nDYVNpEKaw= 195 | github.com/pion/dtls/v3 v3.0.1/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k= 196 | github.com/pion/ice/v3 v3.0.16 h1:YoPlNg3jU1UT/DDTa9v/g1vH6A2/pAzehevI1o66H8E= 197 | github.com/pion/ice/v3 v3.0.16/go.mod h1:SdmubtIsCcvdb1ZInrTUz7Iaqi90/rYd1pzbzlMxsZg= 198 | github.com/pion/ice/v4 v4.0.1 h1:2d3tPoTR90F3TcGYeXUwucGlXI3hds96cwv4kjZmb9s= 199 | github.com/pion/ice/v4 v4.0.1/go.mod h1:2dpakjpd7+74L5j3TAe6gvkbI5UIzOgAnkimm9SuHvA= 200 | github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA= 201 | github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc= 202 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 203 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 204 | github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= 205 | github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= 206 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 207 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 208 | github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= 209 | github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= 210 | github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= 211 | github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= 212 | github.com/pion/sctp v1.8.20 h1:sOc3lkV/tQaP57ZUEXIMdM2V92IIB2ia5v/ygnBxaEg= 213 | github.com/pion/sctp v1.8.20/go.mod h1:oTxw8i5m+WbDHZJL/xUpe6CPIn1Y0GIKKwTLF4h53H8= 214 | github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= 215 | github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= 216 | github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= 217 | github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= 218 | github.com/pion/srtp/v3 v3.0.3 h1:tRtEOpmR8NtsB/KndlKXFOj/AIIs6aPrCq4TlAatC4M= 219 | github.com/pion/srtp/v3 v3.0.3/go.mod h1:Bp9ztzPCoE0ETca/R+bTVTO5kBgaQMiQkTmZWwazDTc= 220 | github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= 221 | github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= 222 | github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= 223 | github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= 224 | github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= 225 | github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= 226 | github.com/pion/transport/v2 v2.2.8 h1:HzsqGBChgtF4Cj47gu51l5hONuK/NwgbZL17CMSuwS0= 227 | github.com/pion/transport/v2 v2.2.8/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 228 | github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= 229 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 230 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 231 | github.com/pion/turn/v3 v3.0.3 h1:1e3GVk8gHZLPBA5LqadWYV60lmaKUaHCkm9DX9CkGcE= 232 | github.com/pion/turn/v3 v3.0.3/go.mod h1:vw0Dz420q7VYAF3J4wJKzReLHIo2LGp4ev8nXQexYsc= 233 | github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= 234 | github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= 235 | github.com/pion/webrtc/v4 v4.0.0-beta.27 h1:dp5xUnzbNSI8VN0yADjrstHI+YBIgSLek28sv03MQzQ= 236 | github.com/pion/webrtc/v4 v4.0.0-beta.27/go.mod h1:EOEk3QX1N2YmCsntm7aMFgqvUfkUyB9NK7PjfXlFBJY= 237 | github.com/pion/webrtc/v4 v4.0.0-beta.29 h1:ahc4r88phf+Y+7YGl20gEfIYQ/eEMzNvd8KOMtxsE1s= 238 | github.com/pion/webrtc/v4 v4.0.0-beta.29/go.mod h1:z1oOHeVfz+XE9bpuXODxIDJw+/TUvENs34YGbQEdB+c= 239 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 240 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 241 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 242 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 243 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 244 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 245 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 246 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 247 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 248 | github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= 249 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 250 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 251 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 252 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 253 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 254 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 255 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 256 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 257 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 258 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 259 | github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug= 260 | github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 261 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 262 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 263 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 264 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 265 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 266 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 267 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 268 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 269 | github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= 270 | github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= 271 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 272 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 273 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 274 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 275 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 276 | github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= 277 | github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 278 | github.com/samber/slog-multi v1.0.3 h1:8wlX8ioZE38h91DwoJBVnC7JfhgwERwlekY+NHsVsv0= 279 | github.com/samber/slog-multi v1.0.3/go.mod h1:TvwgIK4XPBb8Dn18as5uiTHf7in8gN/AtUXsT57UYuo= 280 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 281 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 282 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 283 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 284 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 285 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 286 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 287 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 288 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 289 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 290 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 291 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 292 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 293 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 294 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 295 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 296 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 297 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 298 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 299 | github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= 300 | github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 301 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 302 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 303 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 304 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 305 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 306 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 307 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 308 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 309 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 310 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 311 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 312 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 313 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 314 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 315 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 316 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 317 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 318 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 319 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 320 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 321 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 322 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 323 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 324 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 325 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 326 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 327 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 328 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 329 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 330 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 331 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 332 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 333 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 334 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 335 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 336 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= 337 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 338 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 339 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 340 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 341 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 342 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 343 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 344 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 345 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 346 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 347 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 348 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 349 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 350 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 351 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 352 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 353 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 354 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 355 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 356 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 357 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 358 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 359 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 360 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 361 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 362 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 363 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 364 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 365 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 366 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 367 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 368 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 369 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 370 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 374 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 375 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 376 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 377 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 378 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 379 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 380 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 381 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 382 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 383 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 384 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 385 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 386 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 387 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 388 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 389 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 390 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 391 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 392 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 393 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 394 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 395 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 396 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 397 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 398 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 399 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 400 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 401 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 402 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 403 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 404 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 405 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 406 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 407 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 408 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 409 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 410 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 411 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 412 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 416 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 417 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 418 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 419 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 450 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 451 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 452 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 453 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 454 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 455 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 456 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 457 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 458 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 459 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 460 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 461 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 462 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 463 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 464 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 465 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 466 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 467 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 468 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 469 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 470 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 471 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 472 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 473 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 474 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 475 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 476 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 477 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 478 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 479 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 480 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 481 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 482 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 483 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 484 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 485 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 486 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 487 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 488 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 489 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 490 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 491 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 492 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 493 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 494 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 495 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 496 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 497 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 498 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 499 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 500 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 501 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 502 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 503 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 504 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 505 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 506 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 507 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 508 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 509 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 510 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 511 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 512 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 513 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 514 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 515 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 516 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 517 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 518 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 519 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 520 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 521 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 522 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 523 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 524 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 525 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 526 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 527 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 528 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 529 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 530 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 531 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 532 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 533 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 534 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 535 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 536 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 537 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 538 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 539 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 540 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 541 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 542 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 543 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 544 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 545 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 546 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 547 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 548 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 549 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 550 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 551 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 552 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 553 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 554 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 555 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 556 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 557 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 558 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 559 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 560 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 561 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 562 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 563 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 564 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 565 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 566 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 567 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 568 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 569 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 570 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 571 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 572 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 573 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 574 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 575 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 576 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 577 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 578 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 579 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 580 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 581 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 582 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 583 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 584 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 585 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 586 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 587 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 588 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 589 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 590 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 591 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 592 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 593 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 594 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 595 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 596 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 597 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 598 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 599 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 600 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 601 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 602 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 603 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 604 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 605 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 606 | google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= 607 | google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 608 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 609 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 610 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 611 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 612 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 613 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 614 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 615 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 616 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 617 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 618 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 619 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 620 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 621 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 622 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 623 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 624 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 625 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 626 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 627 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 628 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 629 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 630 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 631 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 632 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 633 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 634 | -------------------------------------------------------------------------------- /iceperf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function usage() { 3 | cat << _EOF_ 4 | Usage: ${0} apikey [options] 5 | apikey Your API key 6 | [options] Any options to pass to the iceperf-agent 7 | 8 | Example: ${0} your-api-key --timer 9 | 10 | _EOF_ 11 | } 12 | 13 | #-- check arguments and environment 14 | if [ "$#" -lt 1 ]; then 15 | echo "Expected at least 1 argument, got $#" 16 | usage 17 | exit 2 18 | fi 19 | 20 | APIKEY=$1 21 | shift 22 | 23 | # Determine the architecture 24 | arch=$(uname -m) 25 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 26 | # Set the appropriate file names and URLs based on the architecture 27 | case "$arch" in 28 | x86_64) 29 | asset_name="iceperf-agent-$os-amd64.tar.gz" 30 | ;; 31 | arm64) 32 | asset_name="iceperf-agent-$os-arm64.tar.gz" 33 | ;; 34 | *) 35 | echo "Unsupported architecture: $arch" 36 | exit 1 37 | ;; 38 | esac 39 | 40 | # Check if wget exists 41 | if ! command -v wget >/dev/null 2>&1; then 42 | echo "wget is not installed. Please install it." 43 | exit 1 44 | fi 45 | 46 | asset_url="https://github.com/nimbleape/iceperf-agent/releases/latest/download/$asset_name" 47 | md5_url="$asset_url.md5" 48 | local_file="$asset_name" 49 | binary_name="iceperf-agent" 50 | 51 | # Clean up any existing files 52 | rm -f "$local_file" || true 53 | 54 | # Download the remote MD5 hash 55 | remote_md5=$(wget -qO- "$md5_url") 56 | 57 | # Extract the MD5 hash from the downloaded file 58 | if [ -f current.md5 ]; then 59 | current_md5=$(cat current.md5) 60 | else 61 | current_md5="" 62 | fi 63 | 64 | # Compare the hashes 65 | echo "Current MD5 is $current_md5" 66 | echo "Remote MD5 is $remote_md5" 67 | 68 | if [ "$current_md5" == "$remote_md5" ] && [ -f "$binary_name" ];then 69 | echo "The file is already up-to-date." 70 | else 71 | echo "The file is outdated or does not exist. Downloading the new version." 72 | rm "$binary_name" || true 73 | rm current.md5 || true 74 | wget -O "$local_file" "$asset_url" 75 | wget -O current.md5 "$md5_url" 76 | echo "Downloaded new client and new current.md5" 77 | 78 | # Extract the downloaded tar.gz file 79 | tar -xzvf "$local_file" 80 | 81 | # Make the binary executable 82 | chmod +x "$binary_name" 83 | fi 84 | 85 | touch current.md5 86 | 87 | echo "Calling $binary_name with API Key" 88 | # Run the binary with the api key 89 | ./"$binary_name" --api-key="${APIKEY}" "$@" 90 | 91 | # Clean up 92 | rm -f "$local_file" -------------------------------------------------------------------------------- /specifications/specifications.go: -------------------------------------------------------------------------------- 1 | package specifications 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/nimbleape/iceperf-agent/adapters" 8 | ) 9 | 10 | /* 11 | The first specification would be tht the user can connect 12 | to a TURN server (initially just one, more options in the future). 13 | The AT should call the server and confirm that we have connected. 14 | 15 | The talking to the server will be handled by a Driver. 16 | */ 17 | 18 | type ServerConnect interface { 19 | Connect() (bool, error) 20 | } 21 | type TURNProvider interface { 22 | GetIceServers() (adapters.IceServersConfig, error) 23 | } 24 | 25 | func ConnectToServerSpecification(t testing.TB, serverConnect ServerConnect) { 26 | connected, err := serverConnect.Connect() 27 | assert.NoError(t, err) 28 | assert.True(t, connected) 29 | } 30 | 31 | func GetIceServersSpecification(t testing.TB, provider TURNProvider) { 32 | is, err := provider.GetIceServers() 33 | assert.NoError(t, err) 34 | assert.True(t, len(is.IceServers) > 0) 35 | } 36 | -------------------------------------------------------------------------------- /stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Stats represents a statistics object 9 | // Stats represents a statistics object 10 | type Stats struct { 11 | TestRunID string `json:"testRunID"` 12 | Labels map[string]string `json:"labels"` 13 | AnswererTimeToReceiveCandidate float64 `json:"answererTimeToReceiveCandidate"` 14 | OffererTimeToReceiveCandidate float64 `json:"offererTimeToReceiveCandidate"` 15 | OffererDcBytesSentTotal float64 `json:"offererDcBytesSentTotal"` 16 | OffererIceTransportBytesSentTotal float64 `json:"offererIceTransportBytesSentTotal"` 17 | OffererIceTransportBytesReceivedTotal float64 `json:"offererIceTransportBytesReceivedTotal"` 18 | AnswererDcBytesReceivedTotal float64 `json:"answererDcBytesReceivedTotal"` 19 | AnswererIceTransportBytesReceivedTotal float64 `json:"answererIceTransportBytesReceivedTotal"` 20 | AnswererIceTransportBytesSentTotal float64 `json:"answererIceTransportBytesSentTotal"` 21 | LatencyFirstPacket float64 `json:"latencyFirstPacket"` 22 | Throughput map[int64]float64 `json:"throughput"` 23 | InstantThroughput map[int64]float64 `json:"instantThroughput"` 24 | ThroughputMax float64 `json:"throughputMax"` 25 | TestRunStartedAt time.Time `json:"testRunStartedAt"` 26 | Provider string `json:"provider"` 27 | Scheme string `json:"scheme"` 28 | Protocol string `json:"protocol"` 29 | Port string `json:"port"` 30 | Node string `json:"node"` 31 | TimeToConnectedState int64 `json:"timeToConnectedState"` 32 | Connected bool `json:"connected"` 33 | } 34 | 35 | // NewStats creates a new Stats object with a given test run ID 36 | func NewStats(testRunID string, testRunStartedAt time.Time) *Stats { 37 | s := &Stats{ 38 | TestRunID: testRunID, 39 | TestRunStartedAt: testRunStartedAt, 40 | Throughput: make(map[int64]float64), // Initialize the Throughput map 41 | InstantThroughput: make(map[int64]float64), // Initialize the Throughput map 42 | Connected: false, 43 | } 44 | 45 | return s 46 | } 47 | 48 | func (s *Stats) SetTimeToConnectedState(t int64) { 49 | s.TimeToConnectedState = t 50 | s.Connected = true 51 | } 52 | 53 | func (s *Stats) SetProvider(st string) { 54 | s.Provider = st 55 | } 56 | 57 | func (s *Stats) SetScheme(st string) { 58 | s.Scheme = st 59 | } 60 | 61 | func (s *Stats) SetProtocol(st string) { 62 | s.Protocol = st 63 | } 64 | 65 | func (s *Stats) SetPort(st string) { 66 | s.Port = st 67 | } 68 | 69 | func (s *Stats) SetNode(st string) { 70 | s.Node = st 71 | } 72 | 73 | func (s *Stats) SetOffererTimeToReceiveCandidate(o float64) { 74 | s.OffererTimeToReceiveCandidate = o 75 | } 76 | 77 | func (s *Stats) SetAnswererTimeToReceiveCandidate(o float64) { 78 | s.AnswererTimeToReceiveCandidate = o 79 | } 80 | 81 | func (s *Stats) SetOffererDcBytesSentTotal(d float64) { 82 | s.OffererDcBytesSentTotal = d 83 | } 84 | 85 | func (s *Stats) SetOffererIceTransportBytesSentTotal(io float64) { 86 | s.OffererIceTransportBytesSentTotal = io 87 | } 88 | 89 | func (s *Stats) SetOffererIceTransportBytesReceivedTotal(io float64) { 90 | s.OffererIceTransportBytesReceivedTotal = io 91 | } 92 | 93 | func (s *Stats) SetAnswererDcBytesReceivedTotal(a float64) { 94 | s.AnswererDcBytesReceivedTotal = a 95 | } 96 | 97 | func (s *Stats) SetAnswererIceTransportBytesReceivedTotal(ia float64) { 98 | s.AnswererIceTransportBytesReceivedTotal = ia 99 | } 100 | 101 | func (s *Stats) SetAnswererIceTransportBytesSentTotal(ia float64) { 102 | s.AnswererIceTransportBytesSentTotal = ia 103 | } 104 | 105 | func (s *Stats) SetLatencyFirstPacket(l float64) { 106 | s.LatencyFirstPacket = l 107 | } 108 | 109 | func (s *Stats) setThroughputMax(l float64) { 110 | if l < s.ThroughputMax { 111 | // New value is lower, don't set it 112 | return 113 | } 114 | s.ThroughputMax = l 115 | } 116 | 117 | func (s *Stats) AddThroughput(tp int64, v float64, v2 float64) { 118 | s.setThroughputMax(v2) 119 | if _, ok := s.Throughput[tp]; !ok { 120 | s.Throughput[tp] = v 121 | } else { 122 | s.Throughput[tp] += v 123 | } 124 | 125 | if _, ok := s.InstantThroughput[tp]; !ok { 126 | s.InstantThroughput[tp] = v2 127 | } else { 128 | s.InstantThroughput[tp] += v2 129 | } 130 | } 131 | 132 | func (s *Stats) CreateLabels() { 133 | s.Labels = map[string]string{ 134 | "provider": s.Provider, 135 | "scheme": s.Scheme, 136 | "protocol": s.Protocol, 137 | "port": s.Port, 138 | "location": s.Node, 139 | } 140 | } 141 | 142 | // ToJSON returns the stats object as a JSON string 143 | func (s *Stats) ToJSON() (string, error) { 144 | jsonBytes, err := json.Marshal(s) 145 | 146 | if err != nil { 147 | return "", err 148 | } 149 | return string(jsonBytes), nil 150 | } 151 | -------------------------------------------------------------------------------- /util/check.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "log" 4 | 5 | func Check(err error) { 6 | if err != nil { 7 | log.Panic(err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const Version = "0.0.1" 4 | --------------------------------------------------------------------------------