[
  {
    "path": ".dockerignore",
    "content": "fabio\ndist/\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @fabiolb/maintainers\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": ""
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "on: [push, pull_request]\nname: Build\npermissions:\n  contents: read\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-go@v5\n        with:\n          go-version-file: 'go.mod'\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v8\n        with:\n          version: v2.5.0\n  build:\n    runs-on: ubuntu-latest\n    needs: [\"golangci\"]\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Install Go\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: 'go.mod'\n          check-latest: true\n          cache: true\n      - name: Set Hosts\n        run: |\n          echo \"127.0.0.1\texample.com example2.com\" | sudo tee -a /etc/hosts\n      - name: Test\n        run: |\n          export PATH=$PATH:$HOME/bin:$HOME/go/bin\n          make github\n"
  },
  {
    "path": ".github/workflows/github-pages.yml",
    "content": "name: github pages\npermissions:\n  contents: write\non:\n  push:\n    branches:\n      - master\njobs:\n  build-deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: true\n      - name: build\n        run: |\n          export PATH=${PATH}:${HOME}/bin\n          make github-pages\n      - name: deploy\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./docs/public\n          cname: fabiolb.net\n"
  },
  {
    "path": ".github/workflows/go-releaser.yml",
    "content": "name: goreleaser\npermissions:\n  contents: write\non:\n  push:\n    tags:\n      - '*'\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version-file: 'go.mod'\n          check-latest: true\n          cache: true\n      - name: Docker Login\n        uses: docker/login-action@v3\n        with:\n          registry: https://index.docker.io/v1/\n          username: ${{ secrets.DOCKER_HUB_USER }}\n          password: ${{ secrets.DOCKER_HUB_TOKEN }}\n      - name: Import GPG key\n        id: import_gpg\n        uses: crazy-max/ghaction-import-gpg@v6\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          passphrase: ${{ secrets.GPG_PASSPHRASE }}\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          distribution: goreleaser\n          version: '~> v2'\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*-amd64\n*.orig\n*.out\n*.p12\n*.pem\n*.pprof\n*.sha256\n*.swp\n*.tar.gz\n*.test\n*.un~\n*.zip\n.DS_Store\n.idea\n.vagrant\nbuild/builds/\nfabio\nfabio.exe\nfabio.sublime-*\ndemo/cert/\n/pkg/\ndist/\n*.app\n*.hugo_build.lock\n*~\n.RELEASE.CHANGELOG.md\n"
  },
  {
    "path": ".golangci.yml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n\nversion: \"2\"\nrun:\n  concurrency: 4\n  allow-parallel-runners: true\n  allow-serial-runners: true\nlinters:\n  default: all\n  disable:\n    - bodyclose\n    - containedctx\n    - copyloopvar\n    - cyclop\n    - depguard\n    - dupl\n    - dupword\n    - embeddedstructfieldcheck\n    - err113\n    - errcheck\n    - errchkjson\n    - errorlint\n    - exhaustive\n    - exhaustruct\n    - forbidigo\n    - forcetypeassert\n    - funcorder\n    - funlen\n    - gochecknoglobals\n    - gochecknoinits\n    - gocognit\n    - goconst\n    - gocritic\n    - gocyclo\n    - godoclint\n    - godot\n    - godox\n    - gosec\n    - govet\n    - ireturn\n    - lll\n    - maintidx\n    - mnd\n    - nakedret\n    - nestif\n    - nilnil\n    - nlreturn\n    - noctx\n    - noinlineerr\n    - nonamedreturns\n    - paralleltest\n    - perfsprint\n    - prealloc\n    - protogetter\n    - revive\n    - tagliatelle\n    - testpackage\n    - thelper\n    - usestdlibvars\n    - usetesting\n    - varnamelen\n    - whitespace\n    - wrapcheck\n    - wsl\n    - wsl_v5\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - demo/*\nformatters:\n  exclusions:\n    generated: lax\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\nbuilds:\n  - binary: fabio\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - darwin\n      - linux\n      - freebsd\n      - netbsd\n      - openbsd\n      - windows\n    goarch:\n      - 386\n      - amd64\n      - arm\n      - arm64\n    goarm:\n      - 7\narchives:\n  - id: bin\n    name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}'\n    formats:\n      - binary\nsource:\n  enabled: true\n  name_template: '{{ .ProjectName }}-{{.Version }}.src'\n  prefix_template: '{{ .ProjectName }}-{{.Version }}/'\nchecksum:\n  name_template: '{{.ProjectName}}-{{.Version}}.sha256'\nsigns:\n  - artifacts: checksum\n    args:\n      - \"--batch\"\n      - \"--local-user\"\n      - \"{{ .Env.GPG_FINGERPRINT }}\"\n      - \"--output\"\n      - \"${signature}\"\n      - \"--detach-sign\"\n      - \"${artifact}\"\ndockers:\n  - dockerfile: Dockerfile-goreleaser\n    use: buildx\n    goos: linux\n    goarch: amd64\n    image_templates:\n      - 'fabiolb/fabio:latest-amd64'\n      - 'fabiolb/fabio:{{ .Version }}-amd64'\n    build_flag_templates:\n      - \"--platform=linux/amd64\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.title={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n    extra_files:\n      - fabio.properties\n  - dockerfile: Dockerfile-goreleaser\n    use: buildx\n    goos: linux\n    goarch: arm64\n    image_templates:\n      - 'fabiolb/fabio:latest-arm64v8'\n      - 'fabiolb/fabio:{{ .Version }}-arm64v8'\n    build_flag_templates:\n      - \"--platform=linux/arm64/v8\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.title={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n    extra_files:\n      - fabio.properties\n  - dockerfile: Dockerfile-goreleaser\n    use: buildx\n    goos: linux\n    goarch: arm\n    goarm: 7\n    image_templates:\n      - 'fabiolb/fabio:latest-armv7'\n      - 'fabiolb/fabio:{{ .Version }}-armv7'\n    build_flag_templates:\n      - \"--platform=linux/arm/v7\"\n      - \"--label=org.opencontainers.image.created={{.Date}}\"\n      - \"--label=org.opencontainers.image.title={{.ProjectName}}\"\n      - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n      - \"--label=org.opencontainers.image.version={{.Version}}\"\n    extra_files:\n      - fabio.properties\ndocker_manifests:\n  - name_template: 'fabiolb/fabio:latest'\n    image_templates:\n      - 'fabiolb/fabio:latest-amd64'\n      - 'fabiolb/fabio:latest-arm64v8'\n      - 'fabiolb/fabio:latest-armv7'\n  - name_template: 'fabiolb/fabio:{{ .Version }}'\n    image_templates:\n      - 'fabiolb/fabio:{{ .Version }}-amd64'\n      - 'fabiolb/fabio:{{ .Version }}-arm64v8'\n      - 'fabiolb/fabio:{{ .Version }}-armv7'\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [v1.6.10](https://github.com/fabiolb/fabio/tree/v1.6.10) (2025-11-24)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.9...v1.6.10)\n\n**Closed issues:**\n\n- Call the fatal function within the goroutine of the main test function [\\#1009](https://github.com/fabiolb/fabio/issues/1009)\n- Support for authentication middleware \\(eg: oauth2-proxy\\) ? [\\#1006](https://github.com/fabiolb/fabio/issues/1006)\n\n**Merged pull requests:**\n\n- Update deps for upstream fixes. [\\#1011](https://github.com/fabiolb/fabio/pull/1011) ([tristanmorgan](https://github.com/tristanmorgan))\n- Reverse IPv4/IPv6 check. [\\#1010](https://github.com/fabiolb/fabio/pull/1010) ([tristanmorgan](https://github.com/tristanmorgan))\n\n## [v1.6.9](https://github.com/fabiolb/fabio/tree/v1.6.9) (2025-10-16)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.8...v1.6.9)\n\n**Merged pull requests:**\n\n- FROM scratch build and update deps. [\\#1008](https://github.com/fabiolb/fabio/pull/1008) ([tristanmorgan](https://github.com/tristanmorgan))\n- feat: add armv7 support for docker images [\\#1007](https://github.com/fabiolb/fabio/pull/1007) ([amd989](https://github.com/amd989))\n\n## [v1.6.8](https://github.com/fabiolb/fabio/tree/v1.6.8) (2025-09-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.7...v1.6.8)\n\n**Closed issues:**\n\n- Health check on port 9999 [\\#995](https://github.com/fabiolb/fabio/issues/995)\n- Multiple entries in proxy.auth do not work as specified in documentation [\\#929](https://github.com/fabiolb/fabio/issues/929)\n- Correct Consul ACL Policy for Fabio [\\#831](https://github.com/fabiolb/fabio/issues/831)\n- Translating `upstream-host/path` to `path.example.com` [\\#801](https://github.com/fabiolb/fabio/issues/801)\n\n**Merged pull requests:**\n\n- Package dependencies updated. [\\#1004](https://github.com/fabiolb/fabio/pull/1004) ([tristanmorgan](https://github.com/tristanmorgan))\n- docs: Fixes bad example for creating multiple basic authorization schemes [\\#1003](https://github.com/fabiolb/fabio/pull/1003) ([steffkelsey](https://github.com/steffkelsey))\n- Enabling many more linters in the pipeline. [\\#999](https://github.com/fabiolb/fabio/pull/999) ([tristanmorgan](https://github.com/tristanmorgan))\n- Fix missing brand logo in routes page [\\#998](https://github.com/fabiolb/fabio/pull/998) ([tristanmorgan](https://github.com/tristanmorgan))\n- Fixing up some web links [\\#997](https://github.com/fabiolb/fabio/pull/997) ([tristanmorgan](https://github.com/tristanmorgan))\n- Fix the ui.routingtable.source.newtab doc. [\\#996](https://github.com/fabiolb/fabio/pull/996) ([tristanmorgan](https://github.com/tristanmorgan))\n- extract only binary from zipfile [\\#994](https://github.com/fabiolb/fabio/pull/994) ([shantanugadgil](https://github.com/shantanugadgil))\n- add option to constrain fabio instance to specific consul namespace [\\#812](https://github.com/fabiolb/fabio/pull/812) ([baabgai](https://github.com/baabgai))\n\n## [v1.6.7](https://github.com/fabiolb/fabio/tree/v1.6.7) (2025-05-30)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.6...v1.6.7)\n\n**Merged pull requests:**\n\n- Bump go.mod pins to latest point release. [\\#993](https://github.com/fabiolb/fabio/pull/993) ([tristanmorgan](https://github.com/tristanmorgan))\n\n## [v1.6.6](https://github.com/fabiolb/fabio/tree/v1.6.6) (2025-05-26)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.5...v1.6.6)\n\n**Implemented enhancements:**\n\n- Add a golang-linter to CI [\\#972](https://github.com/fabiolb/fabio/pull/972) ([aleksraiden](https://github.com/aleksraiden))\n\n**Closed issues:**\n\n- wiki content vs fabio/docs/content ? [\\#986](https://github.com/fabiolb/fabio/issues/986)\n\n**Merged pull requests:**\n\n- Using staticcheck to fix many issues. [\\#992](https://github.com/fabiolb/fabio/pull/992) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update golangci-lint and run yamlfmt. [\\#990](https://github.com/fabiolb/fabio/pull/990) ([tristanmorgan](https://github.com/tristanmorgan))\n- Fix mistake made in \\#988. [\\#989](https://github.com/fabiolb/fabio/pull/989) ([tristanmorgan](https://github.com/tristanmorgan))\n- Actions permissions [\\#988](https://github.com/fabiolb/fabio/pull/988) ([tristanmorgan](https://github.com/tristanmorgan))\n- docs: fix broken link [\\#987](https://github.com/fabiolb/fabio/pull/987) ([marco-m](https://github.com/marco-m))\n- Update dependancies including GoBGP. [\\#985](https://github.com/fabiolb/fabio/pull/985) ([tristanmorgan](https://github.com/tristanmorgan))\n- Add a CODEOWNERS file. [\\#983](https://github.com/fabiolb/fabio/pull/983) ([tristanmorgan](https://github.com/tristanmorgan))\n- Document insensitive prefix matching in the list. [\\#982](https://github.com/fabiolb/fabio/pull/982) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update golang.org/x/net. [\\#980](https://github.com/fabiolb/fabio/pull/980) ([tristanmorgan](https://github.com/tristanmorgan))\n\n## [v1.6.5](https://github.com/fabiolb/fabio/tree/v1.6.5) (2025-02-28)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.4...v1.6.5)\n\n**Implemented enhancements:**\n\n- Unable to load correct certificates if 1 invalid one is in consul k/v [\\#941](https://github.com/fabiolb/fabio/issues/941)\n- Use a Go 1.24.0 [\\#971](https://github.com/fabiolb/fabio/pull/971) ([aleksraiden](https://github.com/aleksraiden))\n- Report all certificate errors instead of stopping at the first. \\(\\#941\\) [\\#964](https://github.com/fabiolb/fabio/pull/964) ([tristanmorgan](https://github.com/tristanmorgan))\n\n**Closed issues:**\n\n- Please bump golang.org/x/sys dependency to enable a build on riscv64-freebsd [\\#927](https://github.com/fabiolb/fabio/issues/927)\n- Fabio is using Datadog reserved tag keys  [\\#923](https://github.com/fabiolb/fabio/issues/923)\n\n**Merged pull requests:**\n\n- Update deps to latest [\\#975](https://github.com/fabiolb/fabio/pull/975) ([aleksraiden](https://github.com/aleksraiden))\n- updating godeps [\\#969](https://github.com/fabiolb/fabio/pull/969) ([aleksraiden](https://github.com/aleksraiden))\n- Update Hugo config to work with version bump in \\#965 [\\#967](https://github.com/fabiolb/fabio/pull/967) ([tristanmorgan](https://github.com/tristanmorgan))\n- Use Alpine3.21 as base docker image [\\#966](https://github.com/fabiolb/fabio/pull/966) ([aleksraiden](https://github.com/aleksraiden))\n- update CI components [\\#965](https://github.com/fabiolb/fabio/pull/965) ([aleksraiden](https://github.com/aleksraiden))\n- README: remove mention to www.kijiji.it \\(decommissioned in 2022\\) [\\#963](https://github.com/fabiolb/fabio/pull/963) ([marco-m-pix4d](https://github.com/marco-m-pix4d))\n- Update dependancies again. [\\#962](https://github.com/fabiolb/fabio/pull/962) ([tristanmorgan](https://github.com/tristanmorgan))\n- Use ParseUint to test for overflow directly [\\#961](https://github.com/fabiolb/fabio/pull/961) ([dcarbone](https://github.com/dcarbone))\n- Fix small typo in DogStatsD config ref. [\\#960](https://github.com/fabiolb/fabio/pull/960) ([tristanmorgan](https://github.com/tristanmorgan))\n- Rebuild CHANGELOG [\\#959](https://github.com/fabiolb/fabio/pull/959) ([tristanmorgan](https://github.com/tristanmorgan))\n- Adds handling of Datadog reserved tag keys [\\#924](https://github.com/fabiolb/fabio/pull/924) ([froque](https://github.com/froque))\n\n## [v1.6.4](https://github.com/fabiolb/fabio/tree/v1.6.4) (2024-11-27)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.3...v1.6.4)\n\n**Closed issues:**\n\n- CI pipeline to run testsuite [\\#949](https://github.com/fabiolb/fabio/issues/949)\n- Fabio not exporting all the metrics with prometheus [\\#947](https://github.com/fabiolb/fabio/issues/947)\n- certificates - cert and ca chain/intermediate [\\#946](https://github.com/fabiolb/fabio/issues/946)\n- This repository is unmaintaned. [\\#944](https://github.com/fabiolb/fabio/issues/944)\n- CVE-2023-44487 HTTP/2 rapid reset [\\#939](https://github.com/fabiolb/fabio/issues/939)\n- TCP no route - cant balance tcp [\\#936](https://github.com/fabiolb/fabio/issues/936)\n- windows: setting logging path in fabio properties [\\#920](https://github.com/fabiolb/fabio/issues/920)\n- Port range in the proxy.addr [\\#529](https://github.com/fabiolb/fabio/issues/529)\n\n**Merged pull requests:**\n\n- Add GoReleaser workflow. [\\#958](https://github.com/fabiolb/fabio/pull/958) ([tristanmorgan](https://github.com/tristanmorgan))\n- go-kit/kit/log go-kit/log [\\#956](https://github.com/fabiolb/fabio/pull/956) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update go-retryablehttp to fix warning. [\\#954](https://github.com/fabiolb/fabio/pull/954) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update and try fix GH Pages publish action. [\\#953](https://github.com/fabiolb/fabio/pull/953) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update go version, test binaries and package versions. [\\#952](https://github.com/fabiolb/fabio/pull/952) ([tristanmorgan](https://github.com/tristanmorgan))\n- Remove vendored modules in favour of go mod. [\\#951](https://github.com/fabiolb/fabio/pull/951) ([tristanmorgan](https://github.com/tristanmorgan))\n- Update Github runner image. [\\#950](https://github.com/fabiolb/fabio/pull/950) ([tristanmorgan](https://github.com/tristanmorgan))\n- fix: close resp body [\\#945](https://github.com/fabiolb/fabio/pull/945) ([testwill](https://github.com/testwill))\n- Trim leading and trailing spaces from service tags [\\#943](https://github.com/fabiolb/fabio/pull/943) ([logocomune](https://github.com/logocomune))\n- Fix doubled download a Vault file [\\#942](https://github.com/fabiolb/fabio/pull/942) ([aleksraiden](https://github.com/aleksraiden))\n- Remove deprecated ioutil [\\#940](https://github.com/fabiolb/fabio/pull/940) ([tristanmorgan](https://github.com/tristanmorgan))\n- Dockerfile: add CAP\\_NET\\_BIND\\_SERVICE+eip to fabio to allow running as root [\\#938](https://github.com/fabiolb/fabio/pull/938) ([Kamilcuk](https://github.com/Kamilcuk))\n- Consul registry performance improvements [\\#928](https://github.com/fabiolb/fabio/pull/928) ([ddreier](https://github.com/ddreier))\n- \\[Docs\\] Fix wrong parameter name [\\#914](https://github.com/fabiolb/fabio/pull/914) ([KEANO89](https://github.com/KEANO89))\n- Updating grpc handler to gracefully close backend connections [\\#913](https://github.com/fabiolb/fabio/pull/913) ([nathanejohnson](https://github.com/nathanejohnson))\n\n## [v1.6.3](https://github.com/fabiolb/fabio/tree/v1.6.3) (2022-12-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.2...v1.6.3)\n\n**Implemented enhancements:**\n\n- Feature request: Make source links in ui interface clickable [\\#901](https://github.com/fabiolb/fabio/issues/901)\n\n**Closed issues:**\n\n- Ignore host=dst when backend is https [\\#916](https://github.com/fabiolb/fabio/issues/916)\n- poll new feature requests [\\#910](https://github.com/fabiolb/fabio/issues/910)\n- Fabio Clustering. [\\#668](https://github.com/fabiolb/fabio/issues/668)\n\n**Merged pull requests:**\n\n- Disable BGP functionality on Windows since gobgp does not support this. [\\#919](https://github.com/fabiolb/fabio/pull/919) ([nathanejohnson](https://github.com/nathanejohnson))\n- updating CHANGELOG [\\#918](https://github.com/fabiolb/fabio/pull/918) ([nathanejohnson](https://github.com/nathanejohnson))\n- Don't use \"dst\" literal as sni name when host=dst is specified on https backends [\\#917](https://github.com/fabiolb/fabio/pull/917) ([nathanejohnson](https://github.com/nathanejohnson))\n- add feature to advertise anycast addresses via BGP [\\#909](https://github.com/fabiolb/fabio/pull/909) ([nathanejohnson](https://github.com/nathanejohnson))\n- Change the shutdown procedure to deregister fabio from the registry and then shutdown the proxy [\\#908](https://github.com/fabiolb/fabio/pull/908) ([martinivanov](https://github.com/martinivanov))\n- Feature/source link [\\#907](https://github.com/fabiolb/fabio/pull/907) ([KTruesdellENA](https://github.com/KTruesdellENA))\n\n## [v1.6.2](https://github.com/fabiolb/fabio/tree/v1.6.2) (2022-09-13)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.1...v1.6.2)\n\n**Closed issues:**\n\n- Update TLS cipher parser to include modern ciphers [\\#903](https://github.com/fabiolb/fabio/issues/903)\n- Custom behavior for the situation when the service has no healthy instances [\\#898](https://github.com/fabiolb/fabio/issues/898)\n\n**Merged pull requests:**\n\n- update README for v1.6.2 release [\\#905](https://github.com/fabiolb/fabio/pull/905) ([nathanejohnson](https://github.com/nathanejohnson))\n- Updating TLS cipher config parser to include TLS 1.3 constants. [\\#904](https://github.com/fabiolb/fabio/pull/904) ([nathanejohnson](https://github.com/nathanejohnson))\n\n## [v1.6.1](https://github.com/fabiolb/fabio/tree/v1.6.1) (2022-07-19)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.0...v1.6.1)\n\n**Implemented enhancements:**\n\n- Multi-DC fabio [\\#115](https://github.com/fabiolb/fabio/issues/115)\n\n**Fixed bugs:**\n\n- Crash: invalid log msg: http2: panic serving CLIENT\\_IP:CLIENT\\_PORT: runtime error: index out of range \\[-1\\] [\\#872](https://github.com/fabiolb/fabio/issues/872)\n\n**Closed issues:**\n\n- admin UI Overrides not working [\\#886](https://github.com/fabiolb/fabio/issues/886)\n- Panic on created prometheus metric name [\\#878](https://github.com/fabiolb/fabio/issues/878)\n- Crash on route update: panic: runtime error: index out of range, diffmatchpatch.\\(\\*DiffMatchPatch\\).DiffCharsToLines [\\#873](https://github.com/fabiolb/fabio/issues/873)\n- Experiencing 502's [\\#862](https://github.com/fabiolb/fabio/issues/862)\n- Fabio immediately drop routes when consul agent unavailable for a while [\\#861](https://github.com/fabiolb/fabio/issues/861)\n- \\[proxy/tls\\] Update supported TLS versions and cipher suites [\\#858](https://github.com/fabiolb/fabio/issues/858)\n- JSON schema is incorrect in website Dest should be Dst [\\#852](https://github.com/fabiolb/fabio/issues/852)\n- \\[question\\] URL for TLS destination [\\#850](https://github.com/fabiolb/fabio/issues/850)\n- \\[Feature\\] Possibility of adding arm/arm64 docker builds. [\\#833](https://github.com/fabiolb/fabio/issues/833)\n\n**Merged pull requests:**\n\n- Release/v1.6.1 [\\#897](https://github.com/fabiolb/fabio/pull/897) ([nathanejohnson](https://github.com/nathanejohnson))\n- setting sni to match host [\\#896](https://github.com/fabiolb/fabio/pull/896) ([KTruesdellENA](https://github.com/KTruesdellENA))\n- Update random picker to use math/rand's Intn function [\\#893](https://github.com/fabiolb/fabio/pull/893) ([nathanejohnson](https://github.com/nathanejohnson))\n- add configurable grpc message sizes to \\#632 [\\#890](https://github.com/fabiolb/fabio/pull/890) ([nathanejohnson](https://github.com/nathanejohnson))\n- add tls13 [\\#889](https://github.com/fabiolb/fabio/pull/889) ([nathanejohnson](https://github.com/nathanejohnson))\n- update materialize bits. see issue \\#886 [\\#888](https://github.com/fabiolb/fabio/pull/888) ([nathanejohnson](https://github.com/nathanejohnson))\n- Moved admin UI assets to use go embed [\\#885](https://github.com/fabiolb/fabio/pull/885) ([nathanejohnson](https://github.com/nathanejohnson))\n- update the custom css [\\#884](https://github.com/fabiolb/fabio/pull/884) ([KTruesdellENA](https://github.com/KTruesdellENA))\n- Bump go-diff dependency version to 1.2.0.  Fixes \\#873 [\\#881](https://github.com/fabiolb/fabio/pull/881) ([nathanejohnson](https://github.com/nathanejohnson))\n- bump HUGO version to 0.101.0 [\\#880](https://github.com/fabiolb/fabio/pull/880) ([nathanejohnson](https://github.com/nathanejohnson))\n- add docs from PR \\#854 to fabio.properties [\\#879](https://github.com/fabiolb/fabio/pull/879) ([nathanejohnson](https://github.com/nathanejohnson))\n- Build multi-arch Docker images for amd64 and arm64 architectures [\\#875](https://github.com/fabiolb/fabio/pull/875) ([vamc19](https://github.com/vamc19))\n- Fix x-forwarded-for header processing for ws connections [\\#860](https://github.com/fabiolb/fabio/pull/860) ([bn0ir](https://github.com/bn0ir))\n- Update registry.backend.md [\\#854](https://github.com/fabiolb/fabio/pull/854) ([webmutation](https://github.com/webmutation))\n- Resulting complete routing table has no 'tags \"a,b\"'  in last line [\\#841](https://github.com/fabiolb/fabio/pull/841) ([hb9cwp](https://github.com/hb9cwp))\n- fixes \\#807 - changes map for grpc connections to be a string key [\\#816](https://github.com/fabiolb/fabio/pull/816) ([nathanejohnson](https://github.com/nathanejohnson))\n- Add command line flag to toggle required consistency on consul reads [\\#811](https://github.com/fabiolb/fabio/pull/811) ([jeremycw](https://github.com/jeremycw))\n- Issue 605 grpc host matching [\\#632](https://github.com/fabiolb/fabio/pull/632) ([tommyalatalo](https://github.com/tommyalatalo))\n\n## [v1.6.0](https://github.com/fabiolb/fabio/tree/v1.6.0) (2022-04-11)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.15...v1.6.0)\n\n**Implemented enhancements:**\n\n- Add support for influxdb metrics [\\#253](https://github.com/fabiolb/fabio/issues/253)\n- Support for prometheus [\\#211](https://github.com/fabiolb/fabio/issues/211)\n- Support dogstatd with tags [\\#165](https://github.com/fabiolb/fabio/issues/165)\n- Riemann metrics support [\\#126](https://github.com/fabiolb/fabio/issues/126)\n- Simple HTTP path prefix replacement [\\#767](https://github.com/fabiolb/fabio/pull/767) ([JamesJJ](https://github.com/JamesJJ))\n\n**Closed issues:**\n\n- Consul Route updates very slow with large numbers of routes [\\#865](https://github.com/fabiolb/fabio/issues/865)\n- Restricting TLS versions [\\#859](https://github.com/fabiolb/fabio/issues/859)\n- \\[admin/ui\\] General updates [\\#856](https://github.com/fabiolb/fabio/issues/856)\n- \\[question\\] - Can Fabio listen on 80/tcp with Nomad [\\#844](https://github.com/fabiolb/fabio/issues/844)\n- Supporting requests in the form of /my-app/page1 [\\#842](https://github.com/fabiolb/fabio/issues/842)\n- Fabio Using Container IPs to create routes [\\#839](https://github.com/fabiolb/fabio/issues/839)\n- All my dynamic routes suddenly vanished! [\\#837](https://github.com/fabiolb/fabio/issues/837)\n- Fabio redirecting to /routes on own service [\\#832](https://github.com/fabiolb/fabio/issues/832)\n- docu fabio configure TLS/SSL\\(HTTPS\\) understanding problem [\\#827](https://github.com/fabiolb/fabio/issues/827)\n- Crash: \\[FATAL\\] 1.5.13. strconv.ParseUint: parsing \":1883;PROTO=TCP-DYNAMIC\": invalid syntax [\\#826](https://github.com/fabiolb/fabio/issues/826)\n- strip doesn't work as expected on redirect [\\#824](https://github.com/fabiolb/fabio/issues/824)\n- Using Fabio with Consul over mTLS [\\#820](https://github.com/fabiolb/fabio/issues/820)\n- Switch to github actions [\\#817](https://github.com/fabiolb/fabio/issues/817)\n- Panic - httputil: ReverseProxy read error during body copy [\\#814](https://github.com/fabiolb/fabio/issues/814)\n- Support for Consul and Vault Namespaces [\\#810](https://github.com/fabiolb/fabio/issues/810)\n- grpc be closed when uninstall service target [\\#807](https://github.com/fabiolb/fabio/issues/807)\n- fabio binary filename for download [\\#805](https://github.com/fabiolb/fabio/issues/805)\n- Add arm64 arch [\\#804](https://github.com/fabiolb/fabio/issues/804)\n- Can Fabio to prefer one ethernet interface for proxy\\_addr? [\\#802](https://github.com/fabiolb/fabio/issues/802)\n- TCP Dynamic Proxy route without specifying exact IP? [\\#797](https://github.com/fabiolb/fabio/issues/797)\n- \\[Question\\] What are opinions on allowing stale reads of Consul Catalog [\\#764](https://github.com/fabiolb/fabio/issues/764)\n- Simple HTTP path prefix replacement [\\#691](https://github.com/fabiolb/fabio/issues/691)\n- Does Fabio support multiple CS Stores per listener? [\\#666](https://github.com/fabiolb/fabio/issues/666)\n- \\[Question\\] Stats - Status code per service [\\#371](https://github.com/fabiolb/fabio/issues/371)\n- Statsd output is not good [\\#327](https://github.com/fabiolb/fabio/issues/327)\n- Send metrics to cloudwatch [\\#326](https://github.com/fabiolb/fabio/issues/326)\n- Mixing of HTTPS proxying and SNI+TCP on a single port [\\#213](https://github.com/fabiolb/fabio/issues/213)\n\n**Merged pull requests:**\n\n- gofmt [\\#870](https://github.com/fabiolb/fabio/pull/870) ([nathanejohnson](https://github.com/nathanejohnson))\n- updating x/sys [\\#869](https://github.com/fabiolb/fabio/pull/869) ([nathanejohnson](https://github.com/nathanejohnson))\n- update go and alpine versions [\\#868](https://github.com/fabiolb/fabio/pull/868) ([Netlims](https://github.com/Netlims))\n- \\#865 Move the route table sort into NewTable so that it only happens once. [\\#867](https://github.com/fabiolb/fabio/pull/867) ([ddreier](https://github.com/ddreier))\n- removing exclusion of arm64 mac build.  Fixes \\#804 [\\#866](https://github.com/fabiolb/fabio/pull/866) ([nathanejohnson](https://github.com/nathanejohnson))\n- Fix example commands in registry.consul.kvpath [\\#864](https://github.com/fabiolb/fabio/pull/864) ([blake](https://github.com/blake))\n- Add IdleConnTimeout configurable for http transport [\\#863](https://github.com/fabiolb/fabio/pull/863) ([aal89](https://github.com/aal89))\n- admin/ui updates: [\\#857](https://github.com/fabiolb/fabio/pull/857) ([dcarbone](https://github.com/dcarbone))\n- Update 2 broken links in documentation [\\#822](https://github.com/fabiolb/fabio/pull/822) ([mig4ng](https://github.com/mig4ng))\n- Fix small typo in README.md [\\#821](https://github.com/fabiolb/fabio/pull/821) ([mig4ng](https://github.com/mig4ng))\n- Add support for github actions [\\#819](https://github.com/fabiolb/fabio/pull/819) ([nathanejohnson](https://github.com/nathanejohnson))\n- Remove golang toolchain name from release binary names [\\#818](https://github.com/fabiolb/fabio/pull/818) ([nathanejohnson](https://github.com/nathanejohnson))\n- we don't use Fabio [\\#813](https://github.com/fabiolb/fabio/pull/813) ([hsmade](https://github.com/hsmade))\n- Updating tcp dynamic proxy to match on routes that are port only [\\#806](https://github.com/fabiolb/fabio/pull/806) ([nathanejohnson](https://github.com/nathanejohnson))\n- Refactor metrics [\\#476](https://github.com/fabiolb/fabio/pull/476) ([magiconair](https://github.com/magiconair))\n\n## [v1.5.15](https://github.com/fabiolb/fabio/tree/v1.5.15) (2020-12-01)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.14...v1.5.15)\n\n**Closed issues:**\n\n- TCP Dynamic Proxy is not releasing ports from deregistered services [\\#796](https://github.com/fabiolb/fabio/issues/796)\n- How to configure log file output path [\\#781](https://github.com/fabiolb/fabio/issues/781)\n\n**Merged pull requests:**\n\n- Updating the default GOGC to 100.  800 proves to be a bit insane. [\\#803](https://github.com/fabiolb/fabio/pull/803) ([nathanejohnson](https://github.com/nathanejohnson))\n- Stop dynamic TCP listener when upstream is no longer available [\\#798](https://github.com/fabiolb/fabio/pull/798) ([fwkz](https://github.com/fwkz))\n- Updating dependencies [\\#794](https://github.com/fabiolb/fabio/pull/794) ([nathanejohnson](https://github.com/nathanejohnson))\n- Update CHANGELOG.md [\\#790](https://github.com/fabiolb/fabio/pull/790) ([stevenscg](https://github.com/stevenscg))\n\n## [v1.5.14](https://github.com/fabiolb/fabio/tree/v1.5.14) (2020-09-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.13...v1.5.14)\n\n**Fixed bugs:**\n\n- %20 in route is causing route mismatch,  regression in 1.5.2 , works with 1.3.7 [\\#347](https://github.com/fabiolb/fabio/issues/347)\n\n**Closed issues:**\n\n- matchingHostNoGlob sometimes returns incorrect matched host [\\#786](https://github.com/fabiolb/fabio/issues/786)\n- Add support for HTTPS+TCP+SNI on the same listener [\\#783](https://github.com/fabiolb/fabio/issues/783)\n- SIGTERM + Gracefully closing connections [\\#782](https://github.com/fabiolb/fabio/issues/782)\n- passing multiple routes via command line [\\#776](https://github.com/fabiolb/fabio/issues/776)\n- Master branch build failing with SECURITY ERROR [\\#769](https://github.com/fabiolb/fabio/issues/769)\n- How to disable client authentication for https? [\\#765](https://github.com/fabiolb/fabio/issues/765)\n- Must Access Control require RemoteAddr matching? [\\#754](https://github.com/fabiolb/fabio/issues/754)\n- Fabio Proxy \\(localhost:9999\\) Showing Blank White Screen [\\#752](https://github.com/fabiolb/fabio/issues/752)\n- Fabio 1.5.13 - no more \"\\[INFO\\] Config updates\" message in the logs [\\#751](https://github.com/fabiolb/fabio/issues/751)\n- Authentication issue. [\\#743](https://github.com/fabiolb/fabio/issues/743)\n- Connecting to HTTPS Upstream service. [\\#738](https://github.com/fabiolb/fabio/issues/738)\n- log.routes.format is broken with 1.5.13 [\\#737](https://github.com/fabiolb/fabio/issues/737)\n- Looking for a new maintainer [\\#735](https://github.com/fabiolb/fabio/issues/735)\n- GRPC Proxy + HTTP Proxy, both useable at the same time? [\\#734](https://github.com/fabiolb/fabio/issues/734)\n- Trace spans all have the same operation name [\\#732](https://github.com/fabiolb/fabio/issues/732)\n-  consul: Error fetching config from /fabio/config. Get  [\\#729](https://github.com/fabiolb/fabio/issues/729)\n- Very frequent 502 errors  [\\#721](https://github.com/fabiolb/fabio/issues/721)\n- Fabio decodes URL path parameters [\\#486](https://github.com/fabiolb/fabio/issues/486)\n- http proxy error context canceled [\\#264](https://github.com/fabiolb/fabio/issues/264)\n\n**Merged pull requests:**\n\n- Fixing issue \\#786 - matchingHostNoGlob sometimes returns incorrect host [\\#787](https://github.com/fabiolb/fabio/pull/787) ([nathanejohnson](https://github.com/nathanejohnson))\n- updating documentation for pending 1.5.14 release [\\#785](https://github.com/fabiolb/fabio/pull/785) ([nathanejohnson](https://github.com/nathanejohnson))\n- https+tcp+sni listener support [\\#784](https://github.com/fabiolb/fabio/pull/784) ([nathanejohnson](https://github.com/nathanejohnson))\n- chore: fix typo in comments [\\#775](https://github.com/fabiolb/fabio/pull/775) ([josgraha](https://github.com/josgraha))\n- \\(docs\\): fixed small error [\\#774](https://github.com/fabiolb/fabio/pull/774) ([0xflotus](https://github.com/0xflotus))\n- Preserve table state by storing buffer table in fixed strings. [\\#749](https://github.com/fabiolb/fabio/pull/749) ([aaronhurt](https://github.com/aaronhurt))\n- only deploy once per build [\\#747](https://github.com/fabiolb/fabio/pull/747) ([aaronhurt](https://github.com/aaronhurt))\n- switch to github pages for doc hosting [\\#746](https://github.com/fabiolb/fabio/pull/746) ([aaronhurt](https://github.com/aaronhurt))\n- minor transition updates and small fixes [\\#745](https://github.com/fabiolb/fabio/pull/745) ([aaronhurt](https://github.com/aaronhurt))\n- switch back to travis CI [\\#744](https://github.com/fabiolb/fabio/pull/744) ([nathanejohnson](https://github.com/nathanejohnson))\n- follow hugo best practices [\\#742](https://github.com/fabiolb/fabio/pull/742) ([aaronhurt](https://github.com/aaronhurt))\n- Documentation updates for project transition. [\\#740](https://github.com/fabiolb/fabio/pull/740) ([aaronhurt](https://github.com/aaronhurt))\n- Fix infinite buffering of SSE responses when gzip is enabled [\\#739](https://github.com/fabiolb/fabio/pull/739) ([ctlajoie](https://github.com/ctlajoie))\n- Add missing \\<svc\\> entry to example route [\\#733](https://github.com/fabiolb/fabio/pull/733) ([BenjaminHerbert](https://github.com/BenjaminHerbert))\n- minor fixups [\\#731](https://github.com/fabiolb/fabio/pull/731) ([aaronhurt](https://github.com/aaronhurt))\n- fix tests [\\#730](https://github.com/fabiolb/fabio/pull/730) ([aaronhurt](https://github.com/aaronhurt))\n- Add HTTP method and path to trace span operation name [\\#715](https://github.com/fabiolb/fabio/pull/715) ([hobochili](https://github.com/hobochili))\n- Deprecate deregisterCriticalServiceAfter option [\\#674](https://github.com/fabiolb/fabio/pull/674) ([pschultz](https://github.com/pschultz))\n- Issue 647 NormalizeHost [\\#648](https://github.com/fabiolb/fabio/pull/648) ([murphymj25](https://github.com/murphymj25))\n- Handle context canceled errors + better http proxy error handling [\\#644](https://github.com/fabiolb/fabio/pull/644) ([danlsgiga](https://github.com/danlsgiga))\n- Added idleTimout to config and to serve.go HTTP server [\\#635](https://github.com/fabiolb/fabio/pull/635) ([galen0624](https://github.com/galen0624))\n- Issue 613 tcp dynamic [\\#626](https://github.com/fabiolb/fabio/pull/626) ([murphymj25](https://github.com/murphymj25))\n- Issue 554 Added compiled glob matching using LRU Cache [\\#615](https://github.com/fabiolb/fabio/pull/615) ([galen0624](https://github.com/galen0624))\n- Issue 558 - Add Polling Interval From Fabio to Consul to Fabio Config [\\#572](https://github.com/fabiolb/fabio/pull/572) ([galen0624](https://github.com/galen0624))\n- Feat: Pass encoded characters in path unchanged [\\#489](https://github.com/fabiolb/fabio/pull/489) ([valentin-krasontovitsch](https://github.com/valentin-krasontovitsch))\n\n## [v1.5.13](https://github.com/fabiolb/fabio/tree/v1.5.13) (2019-11-18)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.12...v1.5.13)\n\n**Closed issues:**\n\n- Fabio 1.5.12 - panic: runtime error: invalid memory address or nil pointer dereference [\\#719](https://github.com/fabiolb/fabio/issues/719)\n- Question: Load balancing WebSocket connections [\\#718](https://github.com/fabiolb/fabio/issues/718)\n- Question: resources \\(css, js files\\) by multiple sites [\\#717](https://github.com/fabiolb/fabio/issues/717)\n- Fabio UI not displaying when hit on a DNS name [\\#712](https://github.com/fabiolb/fabio/issues/712)\n- Unable to route to websites [\\#676](https://github.com/fabiolb/fabio/issues/676)\n- Websocket proxy timeouts [\\#518](https://github.com/fabiolb/fabio/issues/518)\n\n**Merged pull requests:**\n\n- fix nil-pointer dereference in detailed config log [\\#720](https://github.com/fabiolb/fabio/pull/720) ([pschultz](https://github.com/pschultz))\n- Safely handle missing cert from Vault KV store [\\#710](https://github.com/fabiolb/fabio/pull/710) ([dradtke](https://github.com/dradtke))\n\n## [v1.5.12](https://github.com/fabiolb/fabio/tree/v1.5.12) (2019-10-11)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.11...v1.5.12)\n\n**Implemented enhancements:**\n\n- docker swarm some times register eth0 other eth1 [\\#652](https://github.com/fabiolb/fabio/issues/652)\n- config: let registry.consul.register.addr default to ui.addr [\\#658](https://github.com/fabiolb/fabio/pull/658) ([pschultz](https://github.com/pschultz))\n- fix exit status code [\\#637](https://github.com/fabiolb/fabio/pull/637) ([ianic](https://github.com/ianic))\n\n**Closed issues:**\n\n- Example of Vault KV clientca option? [\\#703](https://github.com/fabiolb/fabio/issues/703)\n- tcp proxy not work [\\#702](https://github.com/fabiolb/fabio/issues/702)\n- urlprefix-:3306 proto=tcp   not work [\\#701](https://github.com/fabiolb/fabio/issues/701)\n- https proxy not work [\\#700](https://github.com/fabiolb/fabio/issues/700)\n- the http port is 9999 ,the https port is what? [\\#699](https://github.com/fabiolb/fabio/issues/699)\n- TCP proxy log filled with i/o timeout [\\#696](https://github.com/fabiolb/fabio/issues/696)\n- urlprefix-zzz.xxx.com/api  not work [\\#693](https://github.com/fabiolb/fabio/issues/693)\n- Fabio/Consul route integration [\\#689](https://github.com/fabiolb/fabio/issues/689)\n- Unable to route. [\\#680](https://github.com/fabiolb/fabio/issues/680)\n- Fabio 100% CPU usage due to logging [\\#673](https://github.com/fabiolb/fabio/issues/673)\n- Authorization header leaking to the backend. [\\#671](https://github.com/fabiolb/fabio/issues/671)\n- X-Request-Start header [\\#670](https://github.com/fabiolb/fabio/issues/670)\n- fabio service entries may stay in Consul on dirty exit [\\#663](https://github.com/fabiolb/fabio/issues/663)\n- Can fabio route request by request body [\\#661](https://github.com/fabiolb/fabio/issues/661)\n- Wrong reported HealthCheck-URI using custom -proxy.addr & -ui.addr [\\#657](https://github.com/fabiolb/fabio/issues/657)\n- Clarify documentation HTTP Redirects [\\#656](https://github.com/fabiolb/fabio/issues/656)\n- tcp access control doesn't work [\\#651](https://github.com/fabiolb/fabio/issues/651)\n- Crash on start of watchBackend\\(\\) [\\#650](https://github.com/fabiolb/fabio/issues/650)\n- Remove third-party cookie and script requirements from frontend [\\#642](https://github.com/fabiolb/fabio/issues/642)\n- Build should use included vendor directory with modules [\\#638](https://github.com/fabiolb/fabio/issues/638)\n- Route table UI is broken [\\#628](https://github.com/fabiolb/fabio/issues/628)\n- Possible Memory Leak in WatchBackend [\\#595](https://github.com/fabiolb/fabio/issues/595)\n- Release date for 1.5.11 [\\#592](https://github.com/fabiolb/fabio/issues/592)\n- Fabio and Vault Token Issues [\\#523](https://github.com/fabiolb/fabio/issues/523)\n- UI broken where no internet access. [\\#502](https://github.com/fabiolb/fabio/issues/502)\n- make log compatible with the syslog protocol [\\#397](https://github.com/fabiolb/fabio/issues/397)\n\n**Merged pull requests:**\n\n- Add Vault example to the traffic shaping section. [\\#677](https://github.com/fabiolb/fabio/pull/677) ([jrasell](https://github.com/jrasell))\n- Fix matching priority for host:port tuples [\\#675](https://github.com/fabiolb/fabio/pull/675) ([pschultz](https://github.com/pschultz))\n- Add config option to use 128 bit trace IDs [\\#669](https://github.com/fabiolb/fabio/pull/669) ([gfloyd](https://github.com/gfloyd))\n- register: clean-up fabio service entries in Consul on dirty exit [\\#664](https://github.com/fabiolb/fabio/pull/664) ([pires](https://github.com/pires))\n- Fix SSE by implementing Flusher in responseWriter wrapper [\\#655](https://github.com/fabiolb/fabio/pull/655) ([gfloyd](https://github.com/gfloyd))\n- Use go-sockaddr to parse address strings [\\#653](https://github.com/fabiolb/fabio/pull/653) ([aaronhurt](https://github.com/aaronhurt))\n- ensure absolute path after strip to maintain rfc complaince [\\#645](https://github.com/fabiolb/fabio/pull/645) ([aaronhurt](https://github.com/aaronhurt))\n- Bundle UI assets [\\#643](https://github.com/fabiolb/fabio/pull/643) ([pschultz](https://github.com/pschultz))\n- ui: Remove duplicate destination column [\\#641](https://github.com/fabiolb/fabio/pull/641) ([pschultz](https://github.com/pschultz))\n- use vendor directory when building - fixes \\#638 [\\#639](https://github.com/fabiolb/fabio/pull/639) ([aaronhurt](https://github.com/aaronhurt))\n- Issue 595 watchbackend [\\#629](https://github.com/fabiolb/fabio/pull/629) ([murphymj25](https://github.com/murphymj25))\n- added support for profile/tracing [\\#624](https://github.com/fabiolb/fabio/pull/624) ([galen0624](https://github.com/galen0624))\n\n## [v1.5.11](https://github.com/fabiolb/fabio/tree/v1.5.11) (2019-04-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.11-wrong...v1.5.11)\n\n**Implemented enhancements:**\n\n- Proxy protocol support fo outgoing connections [\\#191](https://github.com/fabiolb/fabio/issues/191)\n\n**Closed issues:**\n\n- Consul blocking queries should be rate limited to avoid spiking loads on server [\\#627](https://github.com/fabiolb/fabio/issues/627)\n- This seems to be a recursive func call.  Is this correct? [\\#625](https://github.com/fabiolb/fabio/issues/625)\n- Bug in consul 1.4.3 [\\#616](https://github.com/fabiolb/fabio/issues/616)\n- \\[question\\] Release date for 1.5.11 [\\#601](https://github.com/fabiolb/fabio/issues/601)\n- Sidebar of the website is a little off [\\#599](https://github.com/fabiolb/fabio/issues/599)\n- wrong use  function strings.HasPrefix\\(\\) in file passsing.go [\\#545](https://github.com/fabiolb/fabio/issues/545)\n- best way to bypass fabio consul integration? [\\#437](https://github.com/fabiolb/fabio/issues/437)\n\n**Merged pull requests:**\n\n- Issue 611 Added Custom API Driven Back end [\\#614](https://github.com/fabiolb/fabio/pull/614) ([galen0624](https://github.com/galen0624))\n- Improved basic auth htpasswd file refresh \\#604 [\\#610](https://github.com/fabiolb/fabio/pull/610) ([mfuterko](https://github.com/mfuterko))\n- Address \\#545 - wrong use function strings.HasPrefix [\\#607](https://github.com/fabiolb/fabio/pull/607) ([mfuterko](https://github.com/mfuterko))\n- docs: fix layout without JS enabled [\\#606](https://github.com/fabiolb/fabio/pull/606) ([pschultz](https://github.com/pschultz))\n- Implement basic auth htpasswd file refresh [\\#604](https://github.com/fabiolb/fabio/pull/604) ([mfuterko](https://github.com/mfuterko))\n- added support for Consul TLS transport [\\#602](https://github.com/fabiolb/fabio/pull/602) ([sev3ryn](https://github.com/sev3ryn))\n- Proxy protocol on outbound tcp, tcp+sni and tcp with tls connection [\\#598](https://github.com/fabiolb/fabio/pull/598) ([mfuterko](https://github.com/mfuterko))\n\n## [v1.5.11-wrong](https://github.com/fabiolb/fabio/tree/v1.5.11-wrong) (2019-02-25)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.10...v1.5.11-wrong)\n\n**Implemented enhancements:**\n\n- Basic authentication on routes [\\#166](https://github.com/fabiolb/fabio/issues/166)\n\n**Fixed bugs:**\n\n- TCP proxy broken since v1.5.8 [\\#524](https://github.com/fabiolb/fabio/issues/524)\n\n**Closed issues:**\n\n- Fabio's routing table empty. Consul indicates registered services with urlprefix- tags [\\#589](https://github.com/fabiolb/fabio/issues/589)\n- HTTP 502 response half of the time [\\#584](https://github.com/fabiolb/fabio/issues/584)\n- tcp+sni route with allow=ip:something does not seem to work [\\#576](https://github.com/fabiolb/fabio/issues/576)\n- Passing args to fabio in nomad task. [\\#567](https://github.com/fabiolb/fabio/issues/567)\n- Change Log entry update  [\\#562](https://github.com/fabiolb/fabio/issues/562)\n- Release date for 1.5.10? [\\#560](https://github.com/fabiolb/fabio/issues/560)\n- Route updates are delayed with large number of services  [\\#558](https://github.com/fabiolb/fabio/issues/558)\n- could the source and destination be clickable in the ui? [\\#508](https://github.com/fabiolb/fabio/issues/508)\n- Support for opentracing [\\#429](https://github.com/fabiolb/fabio/issues/429)\n- Case-insensitive path matching [\\#35](https://github.com/fabiolb/fabio/issues/35)\n\n**Merged pull requests:**\n\n- ui: Fix XSS vulnerability [\\#588](https://github.com/fabiolb/fabio/pull/588) ([pschultz](https://github.com/pschultz))\n- make Dest column into clickable links [\\#587](https://github.com/fabiolb/fabio/pull/587) ([kneufeld](https://github.com/kneufeld))\n- update documentation around the changes to PROXY protocol [\\#583](https://github.com/fabiolb/fabio/pull/583) ([aaronhurt](https://github.com/aaronhurt))\n- address concerns raised while troubleshooting \\#524 [\\#581](https://github.com/fabiolb/fabio/pull/581) ([aaronhurt](https://github.com/aaronhurt))\n- fix ip access rules within tcp proxy - fixes \\#576 [\\#577](https://github.com/fabiolb/fabio/pull/577) ([aaronhurt](https://github.com/aaronhurt))\n- Add GRPC proxy support [\\#575](https://github.com/fabiolb/fabio/pull/575) ([andyroyle](https://github.com/andyroyle))\n- metrics.circonus: Add support for circonus.submissionurl [\\#574](https://github.com/fabiolb/fabio/pull/574) ([stack72](https://github.com/stack72))\n- add http-basic auth reading from a file [\\#573](https://github.com/fabiolb/fabio/pull/573) ([andyroyle](https://github.com/andyroyle))\n- update consul to v1.4.0 - fixes \\#569 [\\#571](https://github.com/fabiolb/fabio/pull/571) ([aaronhurt](https://github.com/aaronhurt))\n- add faq to address \\#490 [\\#568](https://github.com/fabiolb/fabio/pull/568) ([aaronhurt](https://github.com/aaronhurt))\n- Update go.mod for \\#472 [\\#565](https://github.com/fabiolb/fabio/pull/565) ([magiconair](https://github.com/magiconair))\n- Refactor consul service monitor [\\#564](https://github.com/fabiolb/fabio/pull/564) ([magiconair](https://github.com/magiconair))\n- issue 562 update change log glob.matching.disabled [\\#563](https://github.com/fabiolb/fabio/pull/563) ([galen0624](https://github.com/galen0624))\n- Added new case insensitive matcher [\\#553](https://github.com/fabiolb/fabio/pull/553) ([herbrandson](https://github.com/herbrandson))\n- \\[Docs\\] Delete duplicate 'Path Stripping' page [\\#537](https://github.com/fabiolb/fabio/pull/537) ([rkettelerij](https://github.com/rkettelerij))\n- \\#429 issue - OpenTrace zipKin Support  [\\#472](https://github.com/fabiolb/fabio/pull/472) ([galen0624](https://github.com/galen0624))\n\n## [v1.5.10](https://github.com/fabiolb/fabio/tree/v1.5.10) (2018-10-25)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.9...v1.5.10)\n\n**Fixed bugs:**\n\n- Wrong route for multiple matching host glob patterns [\\#506](https://github.com/fabiolb/fabio/issues/506)\n\n**Closed issues:**\n\n- Fabio forcing response header keys upper case [\\#552](https://github.com/fabiolb/fabio/issues/552)\n- Multiple fabio instances load balancing different set of services. [\\#551](https://github.com/fabiolb/fabio/issues/551)\n- Without Consul how can i use Fabio? [\\#549](https://github.com/fabiolb/fabio/issues/549)\n- Performance issue - Glob matching with large number of services in consul [\\#548](https://github.com/fabiolb/fabio/issues/548)\n- Ignore host case when adding and matching routes [\\#542](https://github.com/fabiolb/fabio/issues/542)\n- allow redirect host to be empty [\\#533](https://github.com/fabiolb/fabio/issues/533)\n- Expose Fabio metrics via Prometheus [\\#532](https://github.com/fabiolb/fabio/issues/532)\n- Memory leak in go-metrics library [\\#530](https://github.com/fabiolb/fabio/issues/530)\n- ability to remove headers from the request [\\#528](https://github.com/fabiolb/fabio/issues/528)\n- urlprefix- does not work properly [\\#527](https://github.com/fabiolb/fabio/issues/527)\n- Problem geting fabio routing to its own ui [\\#525](https://github.com/fabiolb/fabio/issues/525)\n- Redirection to default back-end if route not exists [\\#521](https://github.com/fabiolb/fabio/issues/521)\n- Forwarding Uri tag in original request to endpoint [\\#519](https://github.com/fabiolb/fabio/issues/519)\n- Fabio - Manual overrides [\\#515](https://github.com/fabiolb/fabio/issues/515)\n- If consul is behind an ELB with a set timeout, and the connection is timed out by the ELB, subsequent requests from fabio fail [\\#513](https://github.com/fabiolb/fabio/issues/513)\n- Fabio instantly delete route, whereas health check is passing [\\#512](https://github.com/fabiolb/fabio/issues/512)\n- Would it be possible to configure Fabio to watch services with warning state? [\\#509](https://github.com/fabiolb/fabio/issues/509)\n- Headers passed through fabio are modified [\\#505](https://github.com/fabiolb/fabio/issues/505)\n- Fabio -\\> HTTPS -\\> Service ? [\\#503](https://github.com/fabiolb/fabio/issues/503)\n- Tls + sni support for non http traffic?  [\\#499](https://github.com/fabiolb/fabio/issues/499)\n- Static routes in fabio.properties [\\#498](https://github.com/fabiolb/fabio/issues/498)\n- Tests fail with consul \\> 1.0.6 and vault \\> 0.9.6 [\\#494](https://github.com/fabiolb/fabio/issues/494)\n- Question: wildcard hostname support [\\#491](https://github.com/fabiolb/fabio/issues/491)\n- Fabio confi help with multiple proto [\\#490](https://github.com/fabiolb/fabio/issues/490)\n- Add support for Vault 0.10 KV v2 [\\#483](https://github.com/fabiolb/fabio/issues/483)\n- Support \"standard\" Consul envvars [\\#277](https://github.com/fabiolb/fabio/issues/277)\n- Support Consul TLS [\\#276](https://github.com/fabiolb/fabio/issues/276)\n\n**Merged pull requests:**\n\n- Issue \\#548 added enable/disable glob matching [\\#550](https://github.com/fabiolb/fabio/pull/550) ([galen0624](https://github.com/galen0624))\n- Correct the access control feature documentation page [\\#546](https://github.com/fabiolb/fabio/pull/546) ([msvbhat](https://github.com/msvbhat))\n- Add $host pseudo variable [\\#544](https://github.com/fabiolb/fabio/pull/544) ([holtwilkins](https://github.com/holtwilkins))\n- compare host using lowercase [\\#543](https://github.com/fabiolb/fabio/pull/543) ([shantanugadgil](https://github.com/shantanugadgil))\n- Issue \\#530 - Vendored in updated go-metrics package [\\#535](https://github.com/fabiolb/fabio/pull/535) ([galen0624](https://github.com/galen0624))\n- Add setting to flush fabio buffer regardless headers [\\#531](https://github.com/fabiolb/fabio/pull/531) ([samm-git](https://github.com/samm-git))\n- Update README.md [\\#510](https://github.com/fabiolb/fabio/pull/510) ([kuskmen](https://github.com/kuskmen))\n- Issue \\#506: reverse domain names before sorting [\\#507](https://github.com/fabiolb/fabio/pull/507) ([magiconair](https://github.com/magiconair))\n- Fix changelog link in docs footer [\\#500](https://github.com/fabiolb/fabio/pull/500) ([xmikus01](https://github.com/xmikus01))\n- Make tests compatible with Vault 0.10 [\\#497](https://github.com/fabiolb/fabio/pull/497) ([pschultz](https://github.com/pschultz))\n- Delete an unused global variable logOutput [\\#495](https://github.com/fabiolb/fabio/pull/495) ([gua-pian](https://github.com/gua-pian))\n- Add fastcgi handler [\\#435](https://github.com/fabiolb/fabio/pull/435) ([Gufran](https://github.com/Gufran))\n\n## [v1.5.9](https://github.com/fabiolb/fabio/tree/v1.5.9) (2018-05-16)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.8...v1.5.9)\n\n**Closed issues:**\n\n- UI is broken from  versions =\\> 1.7 [\\#487](https://github.com/fabiolb/fabio/issues/487)\n- Building master fails [\\#482](https://github.com/fabiolb/fabio/issues/482)\n- '-registry.consul.register.enabled' does not seem to be respected [\\#467](https://github.com/fabiolb/fabio/issues/467)\n- Access logging fails in combination with proxy gzipping [\\#460](https://github.com/fabiolb/fabio/issues/460)\n- glob matching improvements [\\#452](https://github.com/fabiolb/fabio/issues/452)\n- Add route based on x-forwarded-port header [\\#450](https://github.com/fabiolb/fabio/issues/450)\n- Redirect http to https on the same destination [\\#448](https://github.com/fabiolb/fabio/issues/448)\n- WebSocket Upgrade not sending Response [\\#447](https://github.com/fabiolb/fabio/issues/447)\n- Fabio does not remove service when one of the registered health-checks fail [\\#427](https://github.com/fabiolb/fabio/issues/427)\n- Fabio routing to wrong back end [\\#421](https://github.com/fabiolb/fabio/issues/421)\n- \\[feature\\]: proxy route option [\\#356](https://github.com/fabiolb/fabio/issues/356)\n\n**Merged pull requests:**\n\n- Resetting read deadline [\\#492](https://github.com/fabiolb/fabio/pull/492) ([craigday](https://github.com/craigday))\n- Issue \\#466: make redirect code more robust [\\#477](https://github.com/fabiolb/fabio/pull/477) ([magiconair](https://github.com/magiconair))\n- fix contributors link [\\#475](https://github.com/fabiolb/fabio/pull/475) ([aaronhurt](https://github.com/aaronhurt))\n- ws close on failed handshake \\(\\#421\\) [\\#474](https://github.com/fabiolb/fabio/pull/474) ([magiconair](https://github.com/magiconair))\n- Issue \\#460: Fix access logging when gzip is enabled [\\#470](https://github.com/fabiolb/fabio/pull/470) ([magiconair](https://github.com/magiconair))\n- Fix the regex of the example proxy.gzip.contenttype [\\#468](https://github.com/fabiolb/fabio/pull/468) ([tino](https://github.com/tino))\n- Check upstream X-Forwarded-Proto prior to redirect [\\#466](https://github.com/fabiolb/fabio/pull/466) ([aaronhurt](https://github.com/aaronhurt))\n- Fix certificate stores doc path [\\#458](https://github.com/fabiolb/fabio/pull/458) ([eldondev](https://github.com/eldondev))\n- Add new & improved glob matcher [\\#457](https://github.com/fabiolb/fabio/pull/457) ([sharbov](https://github.com/sharbov))\n- handle indeterminate length proxy chains - fixes \\#449 [\\#453](https://github.com/fabiolb/fabio/pull/453) ([aaronhurt](https://github.com/aaronhurt))\n- Update link for Websockets [\\#446](https://github.com/fabiolb/fabio/pull/446) ([a2ar](https://github.com/a2ar))\n- \"strict\" health-checking \\(\\#427\\) [\\#428](https://github.com/fabiolb/fabio/pull/428) ([systemfreund](https://github.com/systemfreund))\n\n## [v1.5.8](https://github.com/fabiolb/fabio/tree/v1.5.8) (2018-02-18)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.7...v1.5.8)\n\n**Closed issues:**\n\n- TCP Proxying SSH connections [\\#445](https://github.com/fabiolb/fabio/issues/445)\n- route add ... opts \"proto=tcp+sni\" ?? [\\#444](https://github.com/fabiolb/fabio/issues/444)\n- Wildcard registeration issues [\\#440](https://github.com/fabiolb/fabio/issues/440)\n- Feature Request: IP Whitelisting [\\#439](https://github.com/fabiolb/fabio/issues/439)\n- NoRouteHTMLPath not rendering HTML page [\\#438](https://github.com/fabiolb/fabio/issues/438)\n\n**Merged pull requests:**\n\n- ignore fabio.exe [\\#443](https://github.com/fabiolb/fabio/pull/443) ([aaronhurt](https://github.com/aaronhurt))\n- Issue \\#438: Do not add separators for NoRouteHTML page [\\#441](https://github.com/fabiolb/fabio/pull/441) ([magiconair](https://github.com/magiconair))\n- Add option to allow Fabio to register frontend services in Consul on behalf of user services [\\#426](https://github.com/fabiolb/fabio/pull/426) ([rileyje](https://github.com/rileyje))\n- TCP+SNI support arbitrary large Client Hello [\\#423](https://github.com/fabiolb/fabio/pull/423) ([DanSipola](https://github.com/DanSipola))\n\n## [v1.5.7](https://github.com/fabiolb/fabio/tree/v1.5.7) (2018-02-06)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.6...v1.5.7)\n\n**Closed issues:**\n\n- VaultPKI tests fail with go1.10rc1 [\\#434](https://github.com/fabiolb/fabio/issues/434)\n- Ensure that proxy.noroutestatus has three digits [\\#433](https://github.com/fabiolb/fabio/issues/433)\n- Vault PKI documentation and Fabio version [\\#430](https://github.com/fabiolb/fabio/issues/430)\n- configure equivalent  of nginx client\\_max\\_body\\_size [\\#422](https://github.com/fabiolb/fabio/issues/422)\n- \\[question\\] Newbie question: where to place urlpref-host/path? [\\#419](https://github.com/fabiolb/fabio/issues/419)\n- Static / Manual routes management via API [\\#396](https://github.com/fabiolb/fabio/issues/396)\n- Warn if fabio is run as root [\\#369](https://github.com/fabiolb/fabio/issues/369)\n\n**Merged pull requests:**\n\n- Activating Open Collective [\\#432](https://github.com/fabiolb/fabio/pull/432) ([monkeywithacupcake](https://github.com/monkeywithacupcake))\n- fix small typo [\\#431](https://github.com/fabiolb/fabio/pull/431) ([aaronhurt](https://github.com/aaronhurt))\n- Add support for HSTS response headers and provide method for adding additional response headers [\\#425](https://github.com/fabiolb/fabio/pull/425) ([aaronhurt](https://github.com/aaronhurt))\n- Fix maxconn documentation [\\#420](https://github.com/fabiolb/fabio/pull/420) ([slobo](https://github.com/slobo))\n- treat registry.consul.kvpath as prefix [\\#417](https://github.com/fabiolb/fabio/pull/417) ([magiconair](https://github.com/magiconair))\n- Issue \\#369: Do not allow to run fabio as root [\\#377](https://github.com/fabiolb/fabio/pull/377) ([magiconair](https://github.com/magiconair))\n\n## [v1.5.6](https://github.com/fabiolb/fabio/tree/v1.5.6) (2018-01-05)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.5...v1.5.6)\n\n**Closed issues:**\n\n- Excessive consul logging [\\#408](https://github.com/fabiolb/fabio/issues/408)\n- Build new website [\\#405](https://github.com/fabiolb/fabio/issues/405)\n- \\[bug?\\] Fabio uses \"global\" Consul ServiceID's [\\#383](https://github.com/fabiolb/fabio/issues/383)\n\n**Merged pull requests:**\n\n- Issue \\#408: log consul state changes as DEBUG [\\#418](https://github.com/fabiolb/fabio/pull/418) ([magiconair](https://github.com/magiconair))\n- Actually respect -version option [\\#415](https://github.com/fabiolb/fabio/pull/415) ([pschultz](https://github.com/pschultz))\n- Identify services using both the ID and the Node [\\#414](https://github.com/fabiolb/fabio/pull/414) ([alvaroaleman](https://github.com/alvaroaleman))\n\n## [v1.5.5](https://github.com/fabiolb/fabio/tree/v1.5.5) (2017-12-20)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.4...v1.5.5)\n\n**Implemented enhancements:**\n\n- Support custom 404/503 error pages [\\#56](https://github.com/fabiolb/fabio/issues/56)\n\n**Closed issues:**\n\n- Fabio for task/container/service load balancing on amazon ecs with consul and registrator.  [\\#402](https://github.com/fabiolb/fabio/issues/402)\n\n**Merged pull requests:**\n\n- Implement custom noroute html response [\\#398](https://github.com/fabiolb/fabio/pull/398) ([tino](https://github.com/tino))\n\n## [v1.5.4](https://github.com/fabiolb/fabio/tree/v1.5.4) (2017-12-10)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.3...v1.5.4)\n\n**Implemented enhancements:**\n\n- Differentiate \"URL Unavailable/503\" and \"URL Not Found/404\" [\\#214](https://github.com/fabiolb/fabio/issues/214)\n\n**Fixed bugs:**\n\n- opts with host= with multiple routes does not work as expected [\\#385](https://github.com/fabiolb/fabio/issues/385)\n\n**Closed issues:**\n\n- Fabio is not handling SIGHUP \\(HUP\\) signal properly - it dies [\\#400](https://github.com/fabiolb/fabio/issues/400)\n- Typo in manual overrides stops Fabio from updating routes [\\#399](https://github.com/fabiolb/fabio/issues/399)\n- route precendence  [\\#389](https://github.com/fabiolb/fabio/issues/389)\n- how to connect consul cluster [\\#386](https://github.com/fabiolb/fabio/issues/386)\n- Allow comments in manual overrides [\\#379](https://github.com/fabiolb/fabio/issues/379)\n- Domain or protocol redirection [\\#87](https://github.com/fabiolb/fabio/issues/87)\n- Should rewrite the Host Header  [\\#75](https://github.com/fabiolb/fabio/issues/75)\n\n**Merged pull requests:**\n\n- Issue \\#400: ignore SIGHUP [\\#403](https://github.com/fabiolb/fabio/pull/403) ([magiconair](https://github.com/magiconair))\n- Issue \\#389: match exact host before glob matches [\\#390](https://github.com/fabiolb/fabio/pull/390) ([magiconair](https://github.com/magiconair))\n- Issue \\#385: attach options to target instead of route [\\#388](https://github.com/fabiolb/fabio/pull/388) ([magiconair](https://github.com/magiconair))\n- Fix various minor things [\\#382](https://github.com/fabiolb/fabio/pull/382) ([antham](https://github.com/antham))\n- Remove unused variable [\\#381](https://github.com/fabiolb/fabio/pull/381) ([antham](https://github.com/antham))\n- Now setting the X-Forwarded-Host header if not present. Add matching … [\\#380](https://github.com/fabiolb/fabio/pull/380) ([LeReverandNox](https://github.com/LeReverandNox))\n\n## [v1.5.3](https://github.com/fabiolb/fabio/tree/v1.5.3) (2017-11-03)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.2...v1.5.3)\n\n**Implemented enhancements:**\n\n- Drop privileges after start [\\#195](https://github.com/fabiolb/fabio/issues/195)\n- support for adding CORS headers? [\\#110](https://github.com/fabiolb/fabio/issues/110)\n\n**Closed issues:**\n\n- host=www.mydomain.com not working [\\#375](https://github.com/fabiolb/fabio/issues/375)\n- Wildcards in routing path [\\#374](https://github.com/fabiolb/fabio/issues/374)\n- Questions/issues in using overrides [\\#372](https://github.com/fabiolb/fabio/issues/372)\n- nodes and services in maintenance can cause excessive logging [\\#367](https://github.com/fabiolb/fabio/issues/367)\n- Support fabio.properties in Consul KV store [\\#365](https://github.com/fabiolb/fabio/issues/365)\n- Fabio fails to strip the prefix if the url prefix does not start with the strip option value [\\#363](https://github.com/fabiolb/fabio/issues/363)\n- More than one fabio instance decreases system performance. [\\#361](https://github.com/fabiolb/fabio/issues/361)\n- Documentation of the available metrics? [\\#360](https://github.com/fabiolb/fabio/issues/360)\n- select color scheme from config to distinguish environments [\\#359](https://github.com/fabiolb/fabio/issues/359)\n- \\[Feature request\\]: TCP Proxy support different incoming and outbound ports [\\#353](https://github.com/fabiolb/fabio/issues/353)\n- hgfiii [\\#351](https://github.com/fabiolb/fabio/issues/351)\n- statsd - unable to parse line - gf metric [\\#350](https://github.com/fabiolb/fabio/issues/350)\n- Possibility for Docker Image to pass Consul IP and Port as Variable? [\\#346](https://github.com/fabiolb/fabio/issues/346)\n- Ways to have log verbosity [\\#345](https://github.com/fabiolb/fabio/issues/345)\n- Cant disable consul register with -registry.consul.register.enabled=false [\\#342](https://github.com/fabiolb/fabio/issues/342)\n- Glob Matcher is not working for me [\\#341](https://github.com/fabiolb/fabio/issues/341)\n- Strip option has no effect for websockets [\\#330](https://github.com/fabiolb/fabio/issues/330)\n- access logging is not right [\\#322](https://github.com/fabiolb/fabio/issues/322)\n- FATAL error when metrics cannot be delivered [\\#320](https://github.com/fabiolb/fabio/issues/320)\n- http: proxy error: context canceled [\\#318](https://github.com/fabiolb/fabio/issues/318)\n- /api/routes intermittently returns null. [\\#316](https://github.com/fabiolb/fabio/issues/316)\n- what is the tcp writeTimeout? [\\#307](https://github.com/fabiolb/fabio/issues/307)\n\n**Merged pull requests:**\n\n- Issue \\#375: set host header when host option is set [\\#376](https://github.com/fabiolb/fabio/pull/376) ([magiconair](https://github.com/magiconair))\n\n## [v1.5.2](https://github.com/fabiolb/fabio/tree/v1.5.2) (2017-07-24)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.1...v1.5.2)\n\n**Implemented enhancements:**\n\n- Auto-generated Vault certs [\\#135](https://github.com/fabiolb/fabio/issues/135)\n\n**Closed issues:**\n\n- not able to acces the service via fabio. [\\#319](https://github.com/fabiolb/fabio/issues/319)\n\n**Merged pull requests:**\n\n- Fix memory leak in tcp proxy [\\#321](https://github.com/fabiolb/fabio/pull/321) ([Crypto89](https://github.com/Crypto89))\n\n## [v1.5.1](https://github.com/fabiolb/fabio/tree/v1.5.1) (2017-07-06)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.0...v1.5.1)\n\n**Implemented enhancements:**\n\n- Feature: Allow weight tag in Consul [\\#42](https://github.com/fabiolb/fabio/issues/42)\n\n**Fixed bugs:**\n\n- 1.5.0 config compatibility problem  [\\#305](https://github.com/fabiolb/fabio/issues/305)\n\n**Closed issues:**\n\n- Multiple urlprefix [\\#317](https://github.com/fabiolb/fabio/issues/317)\n- Add metrics for TCP and TCP+SNI proxy [\\#306](https://github.com/fabiolb/fabio/issues/306)\n- How to configure TCP correctly \\(proxy.addr, ...\\) [\\#283](https://github.com/fabiolb/fabio/issues/283)\n- Add parameter to vault token renewal [\\#274](https://github.com/fabiolb/fabio/issues/274)\n\n**Merged pull requests:**\n\n- Issue \\#274: Avoid premature vault token renewals [\\#314](https://github.com/fabiolb/fabio/pull/314) ([pschultz](https://github.com/pschultz))\n- Make tests work with vault 0.7.x [\\#313](https://github.com/fabiolb/fabio/pull/313) ([pschultz](https://github.com/pschultz))\n- Fix syntax highlighting in README [\\#311](https://github.com/fabiolb/fabio/pull/311) ([agis](https://github.com/agis))\n\n## [v1.5.0](https://github.com/fabiolb/fabio/tree/v1.5.0) (2017-06-07)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.4...v1.5.0)\n\n**Implemented enhancements:**\n\n- X-Forwarded-Prefix header support [\\#304](https://github.com/fabiolb/fabio/issues/304)\n- read only web ui [\\#302](https://github.com/fabiolb/fabio/issues/302)\n- Sync X-Forwarded-Proto and Forwarded header when possible [\\#296](https://github.com/fabiolb/fabio/issues/296)\n- Using upstream hostname for request [\\#294](https://github.com/fabiolb/fabio/issues/294)\n- Add profiling support [\\#290](https://github.com/fabiolb/fabio/issues/290)\n- TLS and Connection information through headers [\\#280](https://github.com/fabiolb/fabio/issues/280)\n- Support TLS/Ciphersuite configuration options [\\#249](https://github.com/fabiolb/fabio/issues/249)\n\n**Fixed bugs:**\n\n- Support gzip compression for websockets [\\#300](https://github.com/fabiolb/fabio/issues/300)\n\n**Closed issues:**\n\n- Example of proxy.gzip.contenttype configuration [\\#299](https://github.com/fabiolb/fabio/issues/299)\n- Compatibility with 1.8 [\\#297](https://github.com/fabiolb/fabio/issues/297)\n- cert file names and path= not working as documented [\\#293](https://github.com/fabiolb/fabio/issues/293)\n- Multiple SSL certs for same listener [\\#291](https://github.com/fabiolb/fabio/issues/291)\n- HTTPProxy cannot be aware of timeout of waiting response [\\#288](https://github.com/fabiolb/fabio/issues/288)\n- websockets failing with 500 response - running rancher behind fabio [\\#133](https://github.com/fabiolb/fabio/issues/133)\n\n**Merged pull requests:**\n\n- Using upstream hostname for request \\(\\#294\\) [\\#301](https://github.com/fabiolb/fabio/pull/301) ([mitchelldavis](https://github.com/mitchelldavis))\n\n## [v1.4.4](https://github.com/fabiolb/fabio/tree/v1.4.4) (2017-05-08)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.3...v1.4.4)\n\n**Implemented enhancements:**\n\n- Add service name to access log fields [\\#278](https://github.com/fabiolb/fabio/issues/278)\n\n**Fixed bugs:**\n\n- Fabio does not advertise http/1.1 on TLS connections [\\#289](https://github.com/fabiolb/fabio/issues/289)\n- fabio does not start with multiple listen sockets [\\#279](https://github.com/fabiolb/fabio/issues/279)\n- Websocket not working with HTTPS Upstream [\\#271](https://github.com/fabiolb/fabio/issues/271)\n\n**Closed issues:**\n\n- Reload configuration without restarting fabio by SIGHUP or by flag. [\\#286](https://github.com/fabiolb/fabio/issues/286)\n- chunked Transfer-Encoding [\\#284](https://github.com/fabiolb/fabio/issues/284)\n- How to know what opts are supported in a route / consul tag? [\\#270](https://github.com/fabiolb/fabio/issues/270)\n- Question: Support for Consul v0.7.3 Node tags [\\#252](https://github.com/fabiolb/fabio/issues/252)\n\n## [v1.4.3](https://github.com/fabiolb/fabio/tree/v1.4.3) (2017-04-24)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.2...v1.4.3)\n\n**Fixed bugs:**\n\n- Access log cannot be disabled [\\#269](https://github.com/fabiolb/fabio/issues/269)\n\n**Closed issues:**\n\n- Can fabio proxy by hostname? [\\#267](https://github.com/fabiolb/fabio/issues/267)\n- Issues with Haproxy on passthrough mode [\\#266](https://github.com/fabiolb/fabio/issues/266)\n- How to configure HTTPS upstream manually with tlsskipverify [\\#260](https://github.com/fabiolb/fabio/issues/260)\n- HTTPS upstream added as HTTP [\\#259](https://github.com/fabiolb/fabio/issues/259)\n\n**Merged pull requests:**\n\n- Add support for TLSSkipVerify for https consul fabio check [\\#268](https://github.com/fabiolb/fabio/pull/268) ([Ginja](https://github.com/Ginja))\n\n## [v1.4.2](https://github.com/fabiolb/fabio/tree/v1.4.2) (2017-04-10)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.1...v1.4.2)\n\n**Implemented enhancements:**\n\n- Add HTTPS upstream support [\\#181](https://github.com/fabiolb/fabio/issues/181)\n\n**Closed issues:**\n\n- Find the route across the machine, but no response [\\#256](https://github.com/fabiolb/fabio/issues/256)\n\n**Merged pull requests:**\n\n- Allow UI/API to be served over https [\\#258](https://github.com/fabiolb/fabio/pull/258) ([tmessi](https://github.com/tmessi))\n- Add https upstream support [\\#257](https://github.com/fabiolb/fabio/pull/257) ([tmessi](https://github.com/tmessi))\n\n## [v1.4.1](https://github.com/fabiolb/fabio/tree/v1.4.1) (2017-04-04)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4...v1.4.1)\n\n**Implemented enhancements:**\n\n- Add generic TCP proxying support [\\#179](https://github.com/fabiolb/fabio/issues/179)\n- Add tests and timeouts to TCP+SNI proxy [\\#178](https://github.com/fabiolb/fabio/issues/178)\n\n**Closed issues:**\n\n- Is there any option to enable HSTS [\\#254](https://github.com/fabiolb/fabio/issues/254)\n\n## [v1.4](https://github.com/fabiolb/fabio/tree/v1.4) (2017-03-25)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4rc1...v1.4)\n\n## [v1.4rc1](https://github.com/fabiolb/fabio/tree/v1.4rc1) (2017-03-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4beta2...v1.4rc1)\n\n## [v1.4beta2](https://github.com/fabiolb/fabio/tree/v1.4beta2) (2017-03-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4beta1...v1.4beta2)\n\n## [v1.4beta1](https://github.com/fabiolb/fabio/tree/v1.4beta1) (2017-03-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.8...v1.4beta1)\n\n**Implemented enhancements:**\n\n- Start listener after routing table is initialized [\\#248](https://github.com/fabiolb/fabio/issues/248)\n- Support glob host matching [\\#163](https://github.com/fabiolb/fabio/issues/163)\n- Refactor urlprefix tags [\\#111](https://github.com/fabiolb/fabio/issues/111)\n- TCP proxying support [\\#1](https://github.com/fabiolb/fabio/issues/1)\n\n**Closed issues:**\n\n- feature idea: fabio can be configured to only serve consul services with certain tags [\\#245](https://github.com/fabiolb/fabio/issues/245)\n- How does services get in to router table of fabio [\\#237](https://github.com/fabiolb/fabio/issues/237)\n\n## [v1.3.8](https://github.com/fabiolb/fabio/tree/v1.3.8) (2017-02-14)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.7...v1.3.8)\n\n**Implemented enhancements:**\n\n- Retry registry during startup [\\#240](https://github.com/fabiolb/fabio/issues/240)\n- Make route update logging format configurable [\\#238](https://github.com/fabiolb/fabio/issues/238)\n- Support absolute URLs [\\#219](https://github.com/fabiolb/fabio/issues/219)\n\n**Fixed bugs:**\n\n- requests and notfound metric missing [\\#218](https://github.com/fabiolb/fabio/issues/218)\n- fabio 1.3.6 UI displays host and path as 'undefined' in the routes page [\\#217](https://github.com/fabiolb/fabio/issues/217)\n\n**Closed issues:**\n\n- https support [\\#241](https://github.com/fabiolb/fabio/issues/241)\n- Fabio - setup details [\\#235](https://github.com/fabiolb/fabio/issues/235)\n- Not able to connect to fabio UI ... I wonder if I miss any specifics ?. [\\#234](https://github.com/fabiolb/fabio/issues/234)\n- Error in Fabio setup on container where consul-agent \\(client\\) is installed [\\#233](https://github.com/fabiolb/fabio/issues/233)\n- Fabio Connecting error to local consul-agent \\(client\\) [\\#232](https://github.com/fabiolb/fabio/issues/232)\n- Load balancing between multiple service cluster nodes [\\#231](https://github.com/fabiolb/fabio/issues/231)\n- Specify Consul service name in Fabio config [\\#230](https://github.com/fabiolb/fabio/issues/230)\n- caching [\\#228](https://github.com/fabiolb/fabio/issues/228)\n- Links in docs to the Traffic Shaping page are dead [\\#222](https://github.com/fabiolb/fabio/issues/222)\n- Overrides API and GUI save KV Store as wrong name [\\#220](https://github.com/fabiolb/fabio/issues/220)\n\n## [v1.3.7](https://github.com/fabiolb/fabio/tree/v1.3.7) (2017-01-19)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.6...v1.3.7)\n\n**Implemented enhancements:**\n\n- Support deleting routes by tag [\\#201](https://github.com/fabiolb/fabio/issues/201)\n\n**Fixed bugs:**\n\n- Fabio does not serve http2 with go \\>= 1.7 [\\#215](https://github.com/fabiolb/fabio/issues/215)\n- Bad statsd mean metric format [\\#207](https://github.com/fabiolb/fabio/issues/207)\n\n**Closed issues:**\n\n- Fabio is not able to pick service from consul and not able to update routing table. [\\#210](https://github.com/fabiolb/fabio/issues/210)\n\n## [v1.3.6](https://github.com/fabiolb/fabio/tree/v1.3.6) (2017-01-17)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.5...v1.3.6)\n\n**Implemented enhancements:**\n\n- Refactor config loader tests [\\#199](https://github.com/fabiolb/fabio/issues/199)\n- Routing by path [\\#164](https://github.com/fabiolb/fabio/issues/164)\n- Strip prefix in the forwarded request [\\#44](https://github.com/fabiolb/fabio/issues/44)\n\n**Fixed bugs:**\n\n- runtime error: integer divide by zero [\\#186](https://github.com/fabiolb/fabio/issues/186)\n\n**Closed issues:**\n\n- fabio proxy for consul not work, log show no route [\\#212](https://github.com/fabiolb/fabio/issues/212)\n- Consul registration won't disable [\\#209](https://github.com/fabiolb/fabio/issues/209)\n- Fabio hangs for 30+ seconds for 204 response [\\#206](https://github.com/fabiolb/fabio/issues/206)\n- Fabio running using Nomad system scheduler breaks Docker.  [\\#192](https://github.com/fabiolb/fabio/issues/192)\n\n## [v1.3.5](https://github.com/fabiolb/fabio/tree/v1.3.5) (2016-11-30)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.4...v1.3.5)\n\n**Implemented enhancements:**\n\n- fabio --version switch should work just like -v [\\#197](https://github.com/fabiolb/fabio/issues/197)\n- Remove proxy.header.tls header from inbound request [\\#194](https://github.com/fabiolb/fabio/issues/194)\n- Support transparent response body compression [\\#119](https://github.com/fabiolb/fabio/issues/119)\n\n**Fixed bugs:**\n\n- missing 'cs' in map [\\#189](https://github.com/fabiolb/fabio/issues/189)\n- WebSockets not working with IE10 - header casing. [\\#183](https://github.com/fabiolb/fabio/issues/183)\n- Vault CA Certificate [\\#182](https://github.com/fabiolb/fabio/issues/182)\n\n**Closed issues:**\n\n- Logs request [\\#188](https://github.com/fabiolb/fabio/issues/188)\n- Is this the expecting behavior of Fabio with paths? [\\#187](https://github.com/fabiolb/fabio/issues/187)\n- TCP+SNI support on the same port as HTTPS  [\\#169](https://github.com/fabiolb/fabio/issues/169)\n\n## [v1.3.4](https://github.com/fabiolb/fabio/tree/v1.3.4) (2016-10-28)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.3...v1.3.4)\n\n## [v1.3.3](https://github.com/fabiolb/fabio/tree/v1.3.3) (2016-10-12)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.2...v1.3.3)\n\n**Implemented enhancements:**\n\n- Provide linux/arm and linux/arm64 binaries [\\#161](https://github.com/fabiolb/fabio/issues/161)\n- Metrics Prefix with templates [\\#160](https://github.com/fabiolb/fabio/pull/160) ([md2k](https://github.com/md2k))\n\n**Fixed bugs:**\n\n- TCP+SNI proxy does not work with PROXY protocol [\\#177](https://github.com/fabiolb/fabio/issues/177)\n- Consul cert store URL with token not parsed correctly [\\#172](https://github.com/fabiolb/fabio/issues/172)\n- Panic on invalid response [\\#159](https://github.com/fabiolb/fabio/issues/159)\n\n**Closed issues:**\n\n- can not see new application added to the same fabio instance [\\#176](https://github.com/fabiolb/fabio/issues/176)\n- Ridiculous lack for docker documentation [\\#175](https://github.com/fabiolb/fabio/issues/175)\n- OT: logo for the eBay organization [\\#158](https://github.com/fabiolb/fabio/issues/158)\n\n**Merged pull requests:**\n\n- Use Go's net.JoinHostPort which will auto-detect ipv6 [\\#167](https://github.com/fabiolb/fabio/pull/167) ([jovandeginste](https://github.com/jovandeginste))\n\n## [v1.3.2](https://github.com/fabiolb/fabio/tree/v1.3.2) (2016-09-11)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.1...v1.3.2)\n\n**Fixed bugs:**\n\n- ParseListen may set the wrong protocol [\\#157](https://github.com/fabiolb/fabio/issues/157)\n\n## [v1.3.1](https://github.com/fabiolb/fabio/tree/v1.3.1) (2016-09-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3...v1.3.1)\n\n## [v1.3](https://github.com/fabiolb/fabio/tree/v1.3) (2016-09-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2.1...v1.3)\n\n**Implemented enhancements:**\n\n- Add support for Circonus metrics [\\#151](https://github.com/fabiolb/fabio/issues/151)\n- Support multiple metrics libraries [\\#147](https://github.com/fabiolb/fabio/issues/147)\n- Is there a way to prevent SSL requests falling back to an unrelated cert? [\\#138](https://github.com/fabiolb/fabio/issues/138)\n- Vault token should not require 'root' or 'sudo' privileges [\\#134](https://github.com/fabiolb/fabio/issues/134)\n- Extended metrics [\\#125](https://github.com/fabiolb/fabio/issues/125)\n\n**Fixed bugs:**\n\n- fabio fails to start with \"\\[FATAL\\] 1.2. missing 'cs' in cs\" [\\#146](https://github.com/fabiolb/fabio/issues/146)\n\n**Closed issues:**\n\n- fabio g-rpc [\\#156](https://github.com/fabiolb/fabio/issues/156)\n- Routing based on Accept Header [\\#155](https://github.com/fabiolb/fabio/issues/155)\n- not all command-line options seem to do anything [\\#152](https://github.com/fabiolb/fabio/issues/152)\n\n## [v1.2.1](https://github.com/fabiolb/fabio/tree/v1.2.1) (2016-08-25)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2...v1.2.1)\n\n**Implemented enhancements:**\n\n- Server-sent events support [\\#129](https://github.com/fabiolb/fabio/issues/129)\n- access logging [\\#80](https://github.com/fabiolb/fabio/issues/80)\n- Support configuration via command line arguments [\\#79](https://github.com/fabiolb/fabio/issues/79)\n- Support statsd [\\#73](https://github.com/fabiolb/fabio/issues/73)\n- SSL Certs from Vault [\\#70](https://github.com/fabiolb/fabio/issues/70)\n- Refactor listener config [\\#28](https://github.com/fabiolb/fabio/issues/28)\n- Add/remove certificates using API [\\#27](https://github.com/fabiolb/fabio/issues/27)\n\n**Fixed bugs:**\n\n- Always deregister from Consul [\\#136](https://github.com/fabiolb/fabio/issues/136)\n\n**Closed issues:**\n\n- HA access to the management interface on instances [\\#145](https://github.com/fabiolb/fabio/issues/145)\n- Fabio is not adding route, but health check is passing [\\#142](https://github.com/fabiolb/fabio/issues/142)\n- Wrong Destination IP [\\#140](https://github.com/fabiolb/fabio/issues/140)\n- Having trouble recognizing routes from consul [\\#137](https://github.com/fabiolb/fabio/issues/137)\n\n**Merged pull requests:**\n\n- Improve error message on missing trailing slash [\\#143](https://github.com/fabiolb/fabio/pull/143) ([juliangamble](https://github.com/juliangamble))\n- added statsd support [\\#139](https://github.com/fabiolb/fabio/pull/139) ([jshaw86](https://github.com/jshaw86))\n\n## [v1.2](https://github.com/fabiolb/fabio/tree/v1.2) (2016-07-16)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc4...v1.2)\n\n**Fixed bugs:**\n\n- fabio 1.2rc3 panics with -v [\\#128](https://github.com/fabiolb/fabio/issues/128)\n\n## [v1.2rc4](https://github.com/fabiolb/fabio/tree/v1.2rc4) (2016-07-13)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc3...v1.2rc4)\n\n## [v1.2rc3](https://github.com/fabiolb/fabio/tree/v1.2rc3) (2016-07-12)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.6...v1.2rc3)\n\n## [v1.1.6](https://github.com/fabiolb/fabio/tree/v1.1.6) (2016-07-12)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc2...v1.1.6)\n\n**Implemented enhancements:**\n\n- TLS handshake error: failed to verify client's certificate [\\#108](https://github.com/fabiolb/fabio/issues/108)\n\n**Fixed bugs:**\n\n- X-Forwarded-Port should use local port [\\#122](https://github.com/fabiolb/fabio/issues/122)\n\n**Closed issues:**\n\n- Path problem [\\#124](https://github.com/fabiolb/fabio/issues/124)\n\n## [v1.2rc2](https://github.com/fabiolb/fabio/tree/v1.2rc2) (2016-06-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.5...v1.2rc2)\n\n## [v1.1.5](https://github.com/fabiolb/fabio/tree/v1.1.5) (2016-06-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc1...v1.1.5)\n\n**Implemented enhancements:**\n\n- Allow routes to a service in warning status [\\#117](https://github.com/fabiolb/fabio/pull/117) ([erikvanoosten](https://github.com/erikvanoosten))\n\n**Closed issues:**\n\n- Fabio hangs for 30+ seconds for 204 response [\\#120](https://github.com/fabiolb/fabio/issues/120)\n\n## [v1.2rc1](https://github.com/fabiolb/fabio/tree/v1.2rc1) (2016-06-15)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.4...v1.2rc1)\n\n## [v1.1.4](https://github.com/fabiolb/fabio/tree/v1.1.4) (2016-06-15)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3...v1.1.4)\n\n**Implemented enhancements:**\n\n- Custom status code when no route found [\\#107](https://github.com/fabiolb/fabio/issues/107)\n- Keep fabio registered in consul [\\#100](https://github.com/fabiolb/fabio/issues/100)\n- Disable fabio health check in consul [\\#99](https://github.com/fabiolb/fabio/issues/99)\n- Support PROXY protocol [\\#97](https://github.com/fabiolb/fabio/issues/97)\n\n**Closed issues:**\n\n- fabio should expose a /health endpoint  [\\#112](https://github.com/fabiolb/fabio/issues/112)\n- Go 1.5 issue [\\#109](https://github.com/fabiolb/fabio/issues/109)\n\n## [v1.1.3](https://github.com/fabiolb/fabio/tree/v1.1.3) (2016-05-19)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3rc2...v1.1.3)\n\n**Implemented enhancements:**\n\n- Keep sort order in UI stable [\\#104](https://github.com/fabiolb/fabio/issues/104)\n- Trim whitespace around tag [\\#103](https://github.com/fabiolb/fabio/issues/103)\n- SNI support? [\\#85](https://github.com/fabiolb/fabio/issues/85)\n\n## [v1.1.3rc2](https://github.com/fabiolb/fabio/tree/v1.1.3rc2) (2016-05-14)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3rc1...v1.1.3rc2)\n\n**Implemented enhancements:**\n\n- Add glob path matching \\(an alternative to default prefix matching\\) [\\#93](https://github.com/fabiolb/fabio/pull/93) ([dkong](https://github.com/dkong))\n\n## [v1.1.3rc1](https://github.com/fabiolb/fabio/tree/v1.1.3rc1) (2016-05-09)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.2...v1.1.3rc1)\n\n**Implemented enhancements:**\n\n- Improve forward headers [\\#98](https://github.com/fabiolb/fabio/issues/98)\n- Allow tags for fabio service registration [\\#96](https://github.com/fabiolb/fabio/issues/96)\n- Expand experimental HTTP API [\\#95](https://github.com/fabiolb/fabio/issues/95)\n- Drop default port from request [\\#90](https://github.com/fabiolb/fabio/issues/90)\n- Use Address instead of ServiceAddress? [\\#88](https://github.com/fabiolb/fabio/issues/88)\n- Expand ${DC} to consul datacenter [\\#55](https://github.com/fabiolb/fabio/issues/55)\n\n**Closed issues:**\n\n- proxy handler error channel bug? [\\#92](https://github.com/fabiolb/fabio/issues/92)\n\n## [v1.1.2](https://github.com/fabiolb/fabio/tree/v1.1.2) (2016-04-27)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.1...v1.1.2)\n\n**Fixed bugs:**\n\n- Deleted routes hide visible routes [\\#57](https://github.com/fabiolb/fabio/issues/57)\n\n**Closed issues:**\n\n- Recommended way to bind multiple fabio instances to public IP for HA [\\#89](https://github.com/fabiolb/fabio/issues/89)\n- Windows support [\\#86](https://github.com/fabiolb/fabio/issues/86)\n- How to load balance '/'? [\\#83](https://github.com/fabiolb/fabio/issues/83)\n- register websockets with consul tags [\\#82](https://github.com/fabiolb/fabio/issues/82)\n- fabio does not respect registry\\_consul\\_register\\_ip from ENV [\\#77](https://github.com/fabiolb/fabio/issues/77)\n- Not deregistering when consul health status fails  [\\#71](https://github.com/fabiolb/fabio/issues/71)\n- question: configure through environment variables? [\\#68](https://github.com/fabiolb/fabio/issues/68)\n- support middleware\\(OWIN\\) to execute some code before recirection [\\#64](https://github.com/fabiolb/fabio/issues/64)\n\n**Merged pull requests:**\n\n- \\#77 fix documentaion [\\#78](https://github.com/fabiolb/fabio/pull/78) ([sielaq](https://github.com/sielaq))\n- Expose the docker ports in Dockerfile [\\#76](https://github.com/fabiolb/fabio/pull/76) ([smancke](https://github.com/smancke))\n- Overworked header handling. [\\#74](https://github.com/fabiolb/fabio/pull/74) ([smancke](https://github.com/smancke))\n- Broken link corrected. [\\#65](https://github.com/fabiolb/fabio/pull/65) ([jest](https://github.com/jest))\n\n## [v1.1.1](https://github.com/fabiolb/fabio/tree/v1.1.1) (2016-02-22)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1...v1.1.1)\n\n**Merged pull requests:**\n\n- Fix use of local ip in consul service registration [\\#58](https://github.com/fabiolb/fabio/pull/58) ([jeanblanchard](https://github.com/jeanblanchard))\n\n## [v1.1](https://github.com/fabiolb/fabio/tree/v1.1) (2016-02-18)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1rc1...v1.1)\n\n**Implemented enhancements:**\n\n- Make read and write timeout configurable [\\#53](https://github.com/fabiolb/fabio/issues/53)\n\n## [v1.1rc1](https://github.com/fabiolb/fabio/tree/v1.1rc1) (2016-02-15)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.9...v1.1rc1)\n\n## [v1.0.9](https://github.com/fabiolb/fabio/tree/v1.0.9) (2016-02-15)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.8...v1.0.9)\n\n**Implemented enhancements:**\n\n- Allow configuration of serviceip used during consul registration [\\#48](https://github.com/fabiolb/fabio/issues/48)\n- Allow configuration via env vars [\\#43](https://github.com/fabiolb/fabio/issues/43)\n- Cleanup metrics for deleted routes [\\#41](https://github.com/fabiolb/fabio/issues/41)\n- HTTP2 support with latest Go [\\#32](https://github.com/fabiolb/fabio/issues/32)\n- Support additional backends [\\#12](https://github.com/fabiolb/fabio/issues/12)\n\n**Fixed bugs:**\n\n- Include services with check ids other than 'service:\\*' [\\#29](https://github.com/fabiolb/fabio/issues/29)\n\n**Closed issues:**\n\n- Move dependencies to vendor path [\\#47](https://github.com/fabiolb/fabio/issues/47)\n- Add support for Consul ACL token to demo server [\\#37](https://github.com/fabiolb/fabio/issues/37)\n\n## [v1.0.8](https://github.com/fabiolb/fabio/tree/v1.0.8) (2016-01-14)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.7...v1.0.8)\n\n**Implemented enhancements:**\n\n- Consul ACL Token [\\#36](https://github.com/fabiolb/fabio/issues/36)\n\n**Fixed bugs:**\n\n- Detect when consul agent is down [\\#26](https://github.com/fabiolb/fabio/issues/26)\n- fabio route not removed after consul deregister [\\#22](https://github.com/fabiolb/fabio/issues/22)\n\n**Closed issues:**\n\n- Session persistence [\\#33](https://github.com/fabiolb/fabio/issues/33)\n- Build fails on master/last release tag [\\#31](https://github.com/fabiolb/fabio/issues/31)\n- Documentation: make build before running ./fabio [\\#24](https://github.com/fabiolb/fabio/issues/24)\n\n**Merged pull requests:**\n\n- \\[registry\\] fallback to given local IP address [\\#30](https://github.com/fabiolb/fabio/pull/30) ([doublerebel](https://github.com/doublerebel))\n\n## [v1.0.7](https://github.com/fabiolb/fabio/tree/v1.0.7) (2015-12-13)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.6...v1.0.7)\n\n**Fixed bugs:**\n\n- routes not removed when passing empty string [\\#23](https://github.com/fabiolb/fabio/issues/23)\n\n**Closed issues:**\n\n- server demo: Consul health check fails [\\#21](https://github.com/fabiolb/fabio/issues/21)\n- Demo \\(shebang, documentation\\) [\\#20](https://github.com/fabiolb/fabio/issues/20)\n- \\(Docker\\) Error initializing backend. [\\#19](https://github.com/fabiolb/fabio/issues/19)\n\n## [v1.0.6](https://github.com/fabiolb/fabio/tree/v1.0.6) (2015-12-01)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.5...v1.0.6)\n\n**Implemented enhancements:**\n\n- Filter routing table not on tags [\\#16](https://github.com/fabiolb/fabio/issues/16)\n- Support websockets [\\#9](https://github.com/fabiolb/fabio/issues/9)\n\n**Fixed bugs:**\n\n- Traffic shaping does not match on service name [\\#15](https://github.com/fabiolb/fabio/issues/15)\n\n**Closed issues:**\n\n- Manage manual overrides via UI [\\#18](https://github.com/fabiolb/fabio/issues/18)\n\n**Merged pull requests:**\n\n- README: fix typos [\\#14](https://github.com/fabiolb/fabio/pull/14) ([ceh](https://github.com/ceh))\n\n## [v1.0.5](https://github.com/fabiolb/fabio/tree/v1.0.5) (2015-11-11)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.4...v1.0.5)\n\n**Implemented enhancements:**\n\n- Support Forwarded and X-Forwarded-For headers [\\#10](https://github.com/fabiolb/fabio/issues/10)\n\n**Merged pull requests:**\n\n- fix vet warning [\\#13](https://github.com/fabiolb/fabio/pull/13) ([juliendsv](https://github.com/juliendsv))\n\n## [v1.0.4](https://github.com/fabiolb/fabio/tree/v1.0.4) (2015-11-03)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.3...v1.0.4)\n\n**Implemented enhancements:**\n\n- Support SSL/TLS client cert authentication [\\#8](https://github.com/fabiolb/fabio/issues/8)\n\n**Closed issues:**\n\n- List among Consul community tools [\\#6](https://github.com/fabiolb/fabio/issues/6)\n\n**Merged pull requests:**\n\n- Fixes broken fragment identifier link [\\#11](https://github.com/fabiolb/fabio/pull/11) ([budnik](https://github.com/budnik))\n\n## [v1.0.3](https://github.com/fabiolb/fabio/tree/v1.0.3) (2015-10-26)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.2...v1.0.3)\n\n**Merged pull requests:**\n\n- Correcting a typo [\\#5](https://github.com/fabiolb/fabio/pull/5) ([mdevreugd](https://github.com/mdevreugd))\n\n## [v1.0.2](https://github.com/fabiolb/fabio/tree/v1.0.2) (2015-10-23)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.1...v1.0.2)\n\n**Merged pull requests:**\n\n- Honor consul.url and consul.addr from config file [\\#3](https://github.com/fabiolb/fabio/pull/3) ([jeinwag](https://github.com/jeinwag))\n\n## [v1.0.1](https://github.com/fabiolb/fabio/tree/v1.0.1) (2015-10-21)\n\n[Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.0...v1.0.1)\n\n\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nPlease respect others and treat them the way you want to\nbe treated yourself.\n\nIf you feel someone overstepped please reach out to one of the\n[project owners](https://github.com/orgs/fabiolb/teams/owners) and someone will follow up.\n\nThank you\n\nThe Fabio Team\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nMore information on how to contribute to fabio can be found on \nthe [wiki](https://github.com/fabiolb/fabio/wiki/Contributing)\n\n\n## Financial contributions\n\nWe also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/fabio).\nAnyone can file an expense. If the expense makes sense for the development of the community, it will be \"merged\" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.\n\n\n## Credits\n\n\n### Contributors\n\nThank you to all the people who have already contributed to fabio!\n<a href=\"graphs/contributors\"><img src=\"https://opencollective.com/fabio/contributors.svg?width=890\" /></a>\n\n\n### Backers\n\nThank you to all our backers! [[Become a backer](https://opencollective.com/fabio#backer)]\n\n<a href=\"https://opencollective.com/fabio#backers\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/backers.svg?width=890\"></a>\n\n\n### Sponsors\n\nThank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/fabio#sponsor))\n\n<a href=\"https://opencollective.com/fabio/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/fabio/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/fabio/sponsor/9/avatar.svg\"></a>"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang AS build\n\nARG TARGETARCH\nARG consul_version=1.22.0\nADD https://releases.hashicorp.com/consul/${consul_version}/consul_${consul_version}_linux_${TARGETARCH}.zip /usr/local/bin\nRUN cd /usr/local/bin && unzip consul_${consul_version}_linux_${TARGETARCH}.zip consul\n\nARG vault_version=1.21.0\nADD https://releases.hashicorp.com/vault/${vault_version}/vault_${vault_version}_linux_${TARGETARCH}.zip /usr/local/bin\nRUN cd /usr/local/bin && unzip vault_${vault_version}_linux_${TARGETARCH}.zip vault\n\nRUN apt-get update && apt-get install -y git ca-certificates libcap2-bin\nWORKDIR /src\nCOPY . .\nRUN go mod tidy\nRUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags \"-s -w\" -o /src/fabio\nRUN setcap cap_net_bind_service=+ep /src/fabio\nRUN echo \"nobody:x:65534:65534:nobody:/:/sbin/nologin\" > /passwd\nRUN echo \"nogroup:x:65533:\" > /group\n\nFROM scratch\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\nCOPY --from=build /src/fabio /usr/bin/\nCOPY --from=build /passwd /etc/\nCOPY --from=build /group /etc/\nADD --chown=nobody:nogroup fabio.properties /etc/fabio/fabio.properties\nUSER nobody:nogroup\nEXPOSE 9998 9999\nENTRYPOINT [\"/usr/bin/fabio\"]\nCMD [\"-cfg\", \"/etc/fabio/fabio.properties\"]\n"
  },
  {
    "path": "Dockerfile-goreleaser",
    "content": "FROM debian:stable-slim AS build\nRUN apt-get update && apt-get install -y git ca-certificates libcap2-bin\nADD fabio /usr/bin/\nRUN setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/fabio\nRUN echo \"nobody:x:65534:65534:nobody:/:/sbin/nologin\" > /passwd\nRUN echo \"nogroup:x:65533:\" > /group\n\nFROM scratch\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\nCOPY --from=build /usr/bin/fabio /usr/bin/\nCOPY --from=build /passwd /etc/\nCOPY --from=build /group /etc/\nADD --chown=nobody:nogroup fabio.properties /etc/fabio/fabio.properties\nUSER nobody:nogroup\nEXPOSE 9998 9999\nENTRYPOINT [\"/usr/bin/fabio\"]\nCMD [\"-cfg\", \"/etc/fabio/fabio.properties\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Education Networks of America.  All rights reserved.\nCopyright (c) 2017-2020 Frank Schroeder. All rights reserved. (after 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870)\nCopyright (c) 2015-2017 eBay Software Foundation. All rights reserved. (before 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# CUR_TAG is the last git tag plus the delta from the current commit to the tag\n# e.g. v1.5.5-<nr of commits since>-g<current git sha>\nCUR_TAG ?= $(shell git describe --tags --first-parent)\n\n# LAST_TAG is the last git tag\n# e.g. v1.5.5\nLAST_TAG ?= $(shell git describe --tags --first-parent --abbrev=0)\n\n# VERSION is the last git tag without the 'v'\n# e.g. 1.5.5\nVERSION ?= $(shell git describe --tags --first-parent --abbrev=0 | cut -c 2-)\n\n\n# GOFLAGS is the flags for the go compiler.\nGOFLAGS ?= -ldflags \"-X main.version=$(CUR_TAG)\"\n\n# GOVERSION is the current go version, e.g. go1.9.2\nGOVERSION ?= $(shell go version | awk '{print $$3;}')\n\n# GORELEASER is the path to the goreleaser binary.\nGORELEASER ?= $(shell which goreleaser)\n\n# pin versions for CI builds\nCI_CONSUL_VERSION ?= 1.22.0\nCI_VAULT_VERSION ?= 1.21.0\nCI_HUGO_VERSION ?= 0.142.0\nCI_GOBGP_VERSION ?= 3.37.0\n\nBETA_OSES = linux darwin\n\n# all is the default target\nall: test\n\n# help prints a help screen\nhelp:\n\t@echo \"generate  - go generate (use it when updating admin ui assets)\"\n\t@echo \"build     - go build\"\n\t@echo \"install   - go install\"\n\t@echo \"test      - go test\"\n\t@echo \"gofmt     - go fmt\"\n\t@echo \"linux     - go build linux/amd64\"\n\t@echo \"release   - tag, build and publish release with goreleaser\"\n\t@echo \"pkg       - build, test and create pkg/fabio.tar.gz\"\n\t@echo \"clean-adm - remove admin ui assets\"\n\t@echo \"clean     - remove temp files\"\n\t@echo \"clean-all - execute all clean commands\"\n\n# generate executes all go:generate statements\n.PHONY: generate\ngenerate: clean-adm\n\tgo generate $(GOFLAGS) ./...\n\n# build compiles fabio and the test dependencies\n.PHONY: build\nbuild: gofmt\n\tgo build $(GOFLAGS)\n\n# test builds and runs the tests\n.PHONY: test\ntest: build\n\tgo test $(GOFLAGS) -v -test.timeout 15s ./...\n\n# mod performs go module maintenance\n.PHONY: mod\nmod:\n\tgo mod tidy\n\n# gofmt runs gofmt on the code\n.PHONY: gofmt\ngofmt:\n\tgofmt -s -w `find . -type f -name '*.go'`\n\n\nbeta: $(BETA_OSES)\nbeta:\n\tsha256sum fabio_$(CUR_TAG)_*_amd64 > fabio_$(CUR_TAG).sha256 fabio.properties\n\tgpg -b fabio_$(CUR_TAG).sha256\n\ttar czvf fabio_$(CUR_TAG).tar.gz fabio.properties fabio_$(CUR_TAG)_*_amd64 fabio_$(CUR_TAG).sha256 fabio_$(CUR_TAG).sha256.sig\n$(BETA_OSES):\n\tCGO_ENABLED=0 GOOS=$@ GOARCH=amd64 go build -trimpath -tags netgo $(GOFLAGS) -o fabio_$(CUR_TAG)_$@_amd64\n\n# install runs go install\n.PHONY: install\ninstall:\n\tCGO_ENABLED=0 go install -trimpath $(GOFLAGS)\n\n# pkg builds a fabio.tar.gz package with only fabio in it\n.PHONY: pkg\npkg: build test\n\trm -rf pkg\n\tmkdir pkg\n\ttar czf pkg/fabio.tar.gz fabio\n\n# release tags, builds and publishes a build with goreleaser\n#\n# Run this in sub-shells instead of dependencies so that\n# later targets can pick up the new tag value.\n.PHONY: release\nrelease:\n\t$(MAKE) tag\n\t$(MAKE) preflight docker-test gorelease homebrew\n\n# preflight runs some checks before a release\n.PHONY: preflight\npreflight:\n\t[ \"$(CUR_TAG)\" == \"$(LAST_TAG)\" ] || ( echo \"master not tagged. Last tag is $(LAST_TAG)\" ; exit 1 )\n\tgrep -q \"$(LAST_TAG)\" CHANGELOG.md main.go || ( echo \"CHANGELOG.md or main.go not updated. $(LAST_TAG) not found\"; exit 1 )\n\n# tag tags the build\n.PHONY: tag\ntag:\n\tbuild/tag.sh\n\n# gorelease runs goreleaser to build and publish the artifacts\n.PHONY: gorelease .RELEASE.CHANGELOG.md\ngorelease: changelog\n\t[ -x \"$(GORELEASER)\" ] || ( echo \"goreleaser not installed\"; exit 1)\n\tGOVERSION=$(GOVERSION) goreleaser --rm-dist --release-notes=.RELEASE.CHANGELOG.md\n\n.PHONY: goreleasedryrun\ngoreleasedryrun: changelog\n\t[ -x \"$(GORELEASER)\" ] || ( echo \"goreleaser not installed\"; exit 1)\n\tGOVERSION=$(GOVERSION) goreleaser --rm-dist --skip-publish --skip-validate --release-notes=.RELEASE.CHANGELOG.md\n\n.PHONY: changelog\nchangelog:\n\tRELEASE=$(CUR_TAG) build/releasenotes.pl <CHANGELOG.md > .RELEASE.CHANGELOG.md\n# homebrew updates the brew recipe since goreleaser can only\n# handle taps right now.\n.PHONY: homebrew\nhomebrew:\n\tbuild/homebrew.sh $(LAST_TAG)\n\n# docker-test runs make test in a Docker container with\n# pinned versions of the external dependencies\n#\n# We download the binaries outside the Docker build to\n# cache the binaries and prevent repeated downloads since\n# ADD <url> downloads the file every time.\n.PHONY: docker-test\ndocker-test:\n\tdocker build \\\n\t\t--build-arg consul_version=$(CI_CONSUL_VERSION) \\\n\t\t--build-arg vault_version=$(CI_VAULT_VERSION) \\\n\t\t-t test-fabio \\\n\t\t-f Dockerfile \\\n\t\t.\n\n# travis runs tests on Travis CI\n.PHONY: travis\ntravis:\n\twget -q -O ~/consul.zip https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip\n\twget -q -O ~/vault.zip https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip\n\tunzip -o -d ~/bin ~/consul.zip\n\tunzip -o -d ~/bin ~/vault.zip\n\tvault --version\n\tconsul --version\n\tmake test\n\n# travis-pages runs the GitHub pages (https://fabiolb.net/) deploy on Travis CI\n.PHONY: travis-pages\ntravis-pages:\n\twget -q -O ~/hugo.tgz https://github.com/gohugoio/hugo/releases/download/v$(CI_HUGO_VERSION)/hugo_$(CI_HUGO_VERSION)_Linux-64bit.tar.gz\n\ttar -C ~/bin -zxf ~/hugo.tgz hugo\n\thugo version\n\t(cd docs && hugo --verbose)\n\n# github runs tests on github actions\n.PHONY: github\ngithub:\n\twget -q -O ~/consul.zip https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip\n\twget -q -O ~/vault.zip https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip\n\twget -q -O ~/gobgp.tar.gz https://github.com/osrg/gobgp/releases/download/v$(CI_GOBGP_VERSION)/gobgp_$(CI_GOBGP_VERSION)_linux_amd64.tar.gz\n\tunzip -o -d ~/bin ~/consul.zip\n\tunzip -o -d ~/bin ~/vault.zip\n\ttar xzf ~/gobgp.tar.gz -C ~/bin\n\tvault --version\n\tconsul --version\n\tmake test\n\n# github-pages runs the GitHub pages (https://fabiolb.net/) deploy on github actions\n.PHONY: github-pages\ngithub-pages:\n\twget -q -O ~/hugo.tgz https://github.com/gohugoio/hugo/releases/download/v$(CI_HUGO_VERSION)/hugo_$(CI_HUGO_VERSION)_Linux-64bit.tar.gz\n\tmkdir -p ~/bin\n\ttar -C ~/bin -zxf ~/hugo.tgz hugo\n\thugo version\n\t(cd docs && hugo)\n\n# clean-adm cleans up all downloaded assets in admin/ui\n.PHONY: clean-adm\nclean-adm:\n\trm -rf admin/ui/assets/cdnjs.cloudflare.com/ajax/libs/materialize\n\trm -f admin/ui/assets/code.jquery.com/*.js\n\trm -f admin/ui/assets/fonts/material*\n\trm -f admin/ui/assets/fonts/Material*\n\n# clean removes intermediate files\n.PHONY: clean\nclean:\n\tgo clean\n\trm -rf pkg dist fabio\n\tfind . -name '*.test' -delete\n\n.PHONY: all help build test mod gofmt linux install pkg release preflight tag gorelease homebrew docker-test travis travis-pages clean-all beta BETA_OSES\n\n# clean-all executes all \"clean*\" commands\n.PHONY: clean-all\nclean-all: clean clean-adm\n"
  },
  {
    "path": "NOTICES.txt",
    "content": "fabio\n\nhttps://github.com/fabiolb/fabio\nLicense: MIT (https://github.com/fabiolb/fabio/LICENSE)\nCopyright (c) 2017 Frank Schroeder. All rights reserved. (after 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870)\nCopyright (c) 2015 eBay Software Foundation. All rights reserved. (before 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870)\n\n\n------------------------------------------------\nAttribution for Project Dependencies\n------------------------------------------------\n\ngithub.com/armon/go-proxyproto\nhttps://github.com/armon/go-proxyproto\nLicense: MIT (https://github.com/armon/go-proxyproto/LICENSE)\nCopyright (c) 2014 Armon Dadgar\n\n\ngithub.com/circonus-labs/circonus-gometrics\nhttps://github.com/circonus-labs/circonus-gometrics\nLicense: BSD 3-clause (https://github.com/circonus-labs/circonus-gometrics/LICENSE)\nCopyright (c) 2016, Circonus, Inc. All rights reserved.\n\n\ngithub.com/circonus-labs/circonusllhist\nhttps://github.com/circonus-labs/circonusllhist\nLicense: BSD 3-clause (https://github.com/circonus-labs/circonusllhist/LICENSE)\nCopyright (c) 2016 Circonus, Inc. All rights reserved.\n\n\ngithub.com/cyberdelia/go-metrics-graphite\nhttps://github.com/cyberdelia/go-metrics-graphite\nLicense: BSD 2-clause (https://github.com/cyberdelia/go-metrics-graphite/LICENSE)\nCopyright 2015 Timothée Peignier. All rights reserved.\n\n\ngithub.com/fatih/structs\nhttps://github.com/fatih/structs\nLicense: MIT (https://github.com/fatih/structs/LICENSE)\nCopyright (c) 2014 Fatih Arslan\n\n\ngithub.com/hashicorp/consul\nhttps://github.com/hashicorp/consul\nLicense: MPL-2 (https://github.com/hashicorp/consul/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/errwrap\nhttps://github.com/hashicorp/errwrap\nLicense: MPL-2 (https://github.com/hashicorp/errwrap/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/go-cleanhttp\nhttps://github.com/hashicorp/go-cleanhttp\nLicense: MPL-2 (https://github.com/hashicorp/go-cleanhttp/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/go-multierror\nhttps://github.com/hashicorp/go-multierror\nLicense: MPL-2 (https://github.com/hashicorp/go-multierror/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/go-retryablehttp\nhttps://github.com/hashicorp/go-retryablehttp\nLicense: MPL-2 (https://github.com/hashicorp/go-retryablehttp/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/go-rootcerts\nhttps://github.com/hashicorp/go-rootcerts\nLicense: MPL-2 (https://github.com/hashicorp/go-rootcerts/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/hcl\nhttps://github.com/hashicorp/hcl\nLicense: MPL-2 (https://github.com/hashicorp/hcl/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/serf\nhttps://github.com/hashicorp/serf\nLicense: MPL-2 (https://github.com/hashicorp/serf/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/hashicorp/vault\nhttps://github.com/hashicorp/vault\nLicense: MPL-2 (https://github.com/hashicorp/vault/LICENSE)\nCopyright 2017 HashiCorp, Inc.\n\n\ngithub.com/pubnub/go-metrics-statsd\nhttps://github.com/pubnub/go-metrics-statsd\nLicense: MIT (https://github.com/pubnub/go-metrics-statsd/LICENSE)\nCopyright (c) 2016 PubNub\n\n\ngithub.com/magiconair/properties\nhttps://github.com/magiconair/properties\nLicense: BSD 2-clause (https://github.com/magiconair/properties/LICENSE)\nCopyright (c) 2013-2017 - Frank Schroeder\n\n\ngithub.com/mitchellh/go-homedir\nhttps://github.com/mitchellh/go-homedir\nLicense: MIT (https://github.com/mitchellh/go-homedir/LICENSE)\nCopyright (c) 2013 Mitchell Hashimoto\n\n\ngithub.com/mitchellh/mapstructure\nhttps://github.com/mitchellh/mapstructure\nLicense: MIT (https://github.com/mitchellh/mapstructure/LICENSE)\nCopyright (c) 2013 Mitchell Hashimoto\n\n\ngithub.com/rcrowley/go-metrics\nhttps://github.com/rcrowley/go-metrics\nLicense: BSD 2-clause (https://github.com/rcrowley/go-metrics/LICENSE)\nCopyright 2012 Richard Crowley. All rights reserved.\n\n\ngithub.com/ryanuber/go-glob\nhttps://github.com/ryanuber/go-glob\nLicense: MIT (https://github.com/ryanuber/go-glob/LICENSE)\nCopyright (c) 2014 Ryan Uber\n\n\ngithub.com/sergi/go-diff\nhttps://github.com/sergi/go-diff\nLicense: MIT (https://github.com/sergi/go-diff/LICENSE)\nCopyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\n\ngithub.com/rakyll/statik\nhttps://github.com/rakyll/statik\nLicense: Apache-2.0 (https://github.com/rakyll/statik/LICENSE)\nCopyright (c) 2014 Google Inc. All Rights Reserved.\n\n\nMaterializeCSS\nhttps://materializecss.com/\nLicense: MIT (https://github.com/dogfalo/materialize/LICENSE)\nCopyright (c) 2018 Materialize\n\n\njQuery\nhttps://jquery.com/\nLicense: MIT (https://github.com/jquery/jquery/LICENSE)\nCopyright (c) JS Foundation and other contributors, https://js.foundation/\n\n\nMaterial Design icons\nhttp://google.github.io/material-design-icons/\nLicense: Apache-2.0 (https://github.com/google/material-design-icons/LICENSE)\nCopyright 2015 Google, Inc. All Rights Reserved.\n\n\ngolang.org/x/net\nhttps://golang.org/x/net\nLicense: BSD 3-clause (https://golang.org/x/net/LICENSE)\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\n\ngolang.org/go\nhttps://github.com/golang/go\nLicense: BSD 3-clause (https://github.com/golang/go/LICENSE)\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\n\ngithub.com/rogpeppe/fastuuid\nhttps://github.com/rogpeppe/fastuuid.git\nLicense: BSD 3-clause (https://github.com/google/uuid/LICENSE)\nCopyright © 2014, Roger Peppe All rights reserved.\n\ngolang.org/x/sync/singleflight\nhttps://golang.org/x/sync/singleflight\nLicense: BSD 3-clause (https://golang.org/x/sync/LICENSE)\nCopyright (c) 2009 The Go Authors. All rights reserved.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <p align=\"center\" style=\"width: 50%; height: 64px;\">\n    <img src=\"https://cdn.rawgit.com/fabiolb/fabio/015e999/fabio.svg\" height=\"64\"/>\n  </p>\n  <p align=\"center\" style=\"margin-top: 16px\">\n    <a href=\"http://ebay.github.io/\"><img src=\"https://cdn.rawgit.com/fabiolb/fabio/7a02e1f/ebay.png\" height=\"32\" style=\"padding-right: 4px\"/></a>\n    <a href=\"http://www.ebayclassifiedsgroup.com\"><img src=\"https://cdn.rawgit.com/fabiolb/fabio/7a02e1f/ecg.png\" height=\"32\"/></a>\n    <a href=\"http://www.mytaxi.de\"><img src=\"https://cdn.rawgit.com/fabiolb/fabio/7a02e1f/mytaxi.png\" height=\"32\"/></a>\n    <a href=\"http://www.classmarkets.com\"><img src=\"https://cdn.rawgit.com/fabiolb/fabio/7a02e1f/classmarkets.png\" height=\"32\"/></a>\n  </p>\n  <p align=\"center\" style=\"margin-top: 16px\">\n    <a href=\"https://github.com/fabiolb/fabio/releases/latest\"><img alt=\"Release\" src=\"https://img.shields.io/github/release/fabiolb/fabio.svg?style=flat-square\"></a>\n    <a href=\"https://raw.githubusercontent.com/fabiolb/fabio/master/LICENSE\"><img alt=\"License MIT\" src=\"https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square\"></a>\n    <a href=\"https://github.com/fabiolb/fabio/actions/workflows/build.yml\"><img alt=\"Github Actions Build Status\" src=\"https://github.com/fabiolb/fabio/actions/workflows/build.yml/badge.svg\"></a>\n    <a href=\"https://github.com/fabiolb/fabio/releases\"><img alt=\"Downloads\" src=\"https://img.shields.io/github/downloads/fabiolb/fabio/total.svg?style=flat-square\"></a>\n    <a href=\"https://hub.docker.com/r/fabiolb/fabio/\"><img alt=\"Docker Pulls fabiolb\" src=\"https://img.shields.io/docker/pulls/fabiolb/fabio.svg?style=flat-square&label=docker+pulls+fabiolb\"></a>\n  </p>\n</p>\n\n---\n\n#### Notes\n\n1) From release 1.6.1 onward, the minimum golang version supported is 1.16. \n2) From release 1.6.0 onward, metrics backend statsd is no longer supported.  statsd_raw\nworks similarly, though it actually resets counters appropriately.  If you are using datadog,\nyou should consider using the new dogstatsd backend, which has support for tags now.  Graphite\nhistogram functionality has changed slightly since switching to gokit framework, so something to be aware of.\n Prometheus functionality is now supported natively.\n\n3) From release 1.5.15 onward, fabio changes the default GOGC from 800 back to\nthe golang default of 100.  Apparently this made some sense back in the golang 1.5 days, but with\nchanges introduced with golang 1.12 and others, this is probably no longer a very good default.\nThis is still configurable, as always, but the new default should make the most sense for most users.\n\n4) From release 1.5.14, release hashes are signed with a new PGP key.\nSee details [here](https://fabiolb.net/faq/verifying-releases/).\n\n5) From release 1.5.14 onward, fabio binary releases are compiled with golang 1.15+.  \nThis means that the fabio will no longer validate upstream https certificates that do \nnot have SAN extensions matching the server name.  This may be a concern if fabio is \ncommunicating with https backends with misconfigured certificates.  If this is a problem,\nyou can specify `tlsskipverify=true` on the route.\n\n\n\n---\n\nfabio is a fast, modern, zero-conf load balancing HTTP(S) and TCP router\nfor deploying applications managed by [consul](https://consul.io/).\n\nRegister your services in consul, provide a health check and fabio will start\nrouting traffic to them. No configuration required. Deployment, upgrading and\nrefactoring has never been easier.\n\nfabio is developed and maintained by The Fabio Authors.\n\nIt powers some of the largest websites in\nAustralia ([gumtree.com.au](http://www.gumtree.com.au)).\nIt delivers 23.000 req/sec every day since Sep 2015 without problems.\n\nIt integrates with\n[Consul](https://consul.io/),\n[Vault](https://vaultproject.io/),\n[Amazon ELB](https://aws.amazon.com/elasticloadbalancing),\n[Amazon API Gateway](https://aws.amazon.com/api-gateway/)\nand more.\n\nIt supports ([Full feature list](https://fabiolb.net/feature/))\n\n* [TLS termination with dynamic certificate stores](https://fabiolb.net/feature/certificate-stores/)\n* [Raw TCP proxy](https://fabiolb.net/feature/tcp-proxy/)\n* [TCP+SNI proxy for full end-to-end TLS](https://fabiolb.net/feature/tcp-sni-proxy/) without decryption\n* [HTTPS+TCP+SNI proxy for TCP+SNI with HTTPS fallback](https://fabiolb.net/feature/https-tcp-sni-proxy/)\n* [TCP dynamic proxy](https://fabiolb.net/feature/tcp-dynamic-proxy/)\n* [HTTPS upstream support](https://fabiolb.net/feature/https-upstream/)\n* [Websockets](https://fabiolb.net/feature/websockets/) and\n* [SSE](https://fabiolb.net/feature/sse/)\n* [Dynamic reloading without restart](https://fabiolb.net/feature/dynamic-reloading/)\n* [Traffic shaping](https://fabiolb.net/feature/traffic-shaping/) for \"blue/green\" deployments,\n* [Prometheus](https://fabiolb.net/feature/metrics/),\n* [Circonus](https://fabiolb.net/feature/metrics/),\n* [Graphite](https://fabiolb.net/feature/metrics/),\n* [StatsD](https://fabiolb.net/feature/metrics/),\n* [DataDog](https://fabiolb.net/feature/metrics/) for metrics,\n* [WebUI](https://fabiolb.net/feature/web-ui/) and\n* [Advertising BGP anycast addresses](https://fabiolb.net/feature/bgp/) on non-windows platforms.\n\n[Watch](https://www.youtube.com/watch?v=gf43TcWjBrE&list=PL81sUbsFNc5b-Gd59Lpz7BW0eHJBt0GvE&index=1)\nKelsey Hightower demo Consul, Nomad, Vault and fabio at HashiConf EU 2016.\n\nThe full documentation is on [fabiolb.net](https://fabiolb.net/)\n\n## Getting started\n\n1. Install from source, [binary](https://github.com/fabiolb/fabio/releases),\n   [Docker](https://hub.docker.com/r/fabiolb/fabio/) or [Homebrew](http://brew.sh).\n    ```shell\n\t# go 1.15 or higher is required\n    go install github.com/fabiolb/fabio@latest          (>= go1.15)\n\n    brew install fabio                                  (OSX/macOS stable)\n    brew install --devel fabio                          (OSX/macOS devel)\n\n    docker pull fabiolb/fabio                           (Docker)\n\n    https://github.com/fabiolb/fabio/releases           (pre-built binaries)\n    ```\n\n2. Register your service in [consul](https://consul.io/).\n\n   Make sure that each instance registers with a **unique ServiceID** and a service name **without spaces**.\n\n3. Register a **health check** in consul as described [here](https://consul.io/docs/agent/checks.html).\n\n   By default fabio only watches services which have a **passing** health check, unless overridden with [registry.consul.service.status](https://fabiolb.net/ref/registry.consul.service.status/).\n\n4. Register one `urlprefix-` tag per `host/path` prefix it serves, e.g.:\n\n```\n# HTTP/S examples\nurlprefix-/css                                     # path route\nurlprefix-i.com/static                             # host specific path route\nurlprefix-mysite.com/                              # host specific catch all route\nurlprefix-/foo/bar strip=/foo                      # path stripping (forward '/bar' to upstream)\nurlprefix-/foo/bar proto=https                     # HTTPS upstream\nurlprefix-/foo/bar proto=https tlsskipverify=true  # HTTPS upstream and self-signed cert\n\n# TCP examples\nurlprefix-:3306 proto=tcp                          # route external port 3306\n```\n\n   Make sure the prefix for HTTP routes contains **at least one slash** (`/`).\n\n   See the full list of options in the [Documentation](https://github.com/fabiolb/fabio/wiki/Routing#config-language).\n\n5. Start fabio without a config file (assuming a running consul agent on `localhost:8500`)\n   Watch the log output how fabio picks up the route to your service.\n   Try starting/stopping your service to see how the routing table changes instantly.\n\n6. Send all your HTTP traffic to fabio on port `9999`.\n   For TCP proxying see [TCP proxy](https://fabiolb.net/feature/tcp-proxy/).\n\n7. Done\n\n## Author and Founder\n\n* Frank Schroeder [@magiconair](https://twitter.com/magiconair)\n\n## Maintainers\n\n* [Education Networks of America](https://github.com/myENA/)\n* [Fabio Members](https://github.com/orgs/fabiolb/people)\n\n### Contributors\n\nThis project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].\n\n## License\n\n* Contributions up to 14 Apr 2017 before [38f73da](https://github.com/fabiolb/fabio/commit/38f73da6413b68fed1631101ac1d0b79a2fac870)\n\n  MIT Licensed\n  Copyright (c) 2017 eBay Software Foundation. All rights reserved.\n\n* Contributions after 14 Apr 2017 starting with  [38f73da](https://github.com/fabiolb/fabio/commit/38f73da6413b68fed1631101ac1d0b79a2fac870)\n\n  MIT Licensed\n  Copyright (c) 2017-2019 Frank Schroeder. All rights reserved.\n\n* Contributions after 22 Jan 2020 starting with [9da7b1b](https://github.com/fabiolb/fabio/commit/9da7b1b6ce0f631f7974e8663b34022c3496dca7#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5)\n\n  MIT Licensed\n  Copyright (c) 2020 Education Networks of America.  All rights reserved.\n\nSee [LICENSE](https://github.com/fabiolb/fabio/blob/master/LICENSE) for details.\n\n"
  },
  {
    "path": "admin/api/api.go",
    "content": "// Package api provides the HTTP api.\npackage api\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc writeJSON(w http.ResponseWriter, r *http.Request, v interface{}) {\n\t_, pretty := r.URL.Query()[\"pretty\"]\n\n\tvar buf []byte\n\tvar err error\n\tif pretty {\n\t\tbuf, err = json.MarshalIndent(v, \"\", \"    \")\n\t} else {\n\t\tbuf, err = json.Marshal(v)\n\t}\n\n\tif err != nil {\n\t\tlog.Print(\"[ERROR] \", err)\n\t\thttp.Error(w, \"internal error\", 500)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\tw.Write(buf)\n}\n"
  },
  {
    "path": "admin/api/config.go",
    "content": "package api\n\nimport \"net/http\"\n\ntype ConfigHandler struct {\n\tConfig interface{}\n}\n\nfunc (h *ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\twriteJSON(w, r, h.Config)\n}\n"
  },
  {
    "path": "admin/api/manual.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/fabiolb/fabio/registry\"\n)\n\n// ManualHandler provides a fetch and update handler for the manual overrides api.\ntype ManualHandler struct {\n\tBasePath string\n}\n\ntype manual struct {\n\tValue   string `json:\"value\"`\n\tVersion uint64 `json:\"version,string\"`\n}\n\nfunc (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\t// we need this for testing.\n\t// under normal circumstances this is never nil\n\tif registry.Default == nil {\n\t\treturn\n\t}\n\n\tpath := r.RequestURI[len(h.BasePath):]\n\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tvalue, version, err := registry.Default.ReadManual(path)\n\t\tif err != nil {\n\t\t\tlog.Print(\"[ERROR] \", err)\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\twriteJSON(w, r, manual{value, version})\n\t\treturn\n\n\tcase \"PUT\":\n\t\tvar m manual\n\t\tif err := json.NewDecoder(r.Body).Decode(&m); err != nil {\n\t\t\tlog.Print(\"[ERROR] \", err)\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tdefer r.Body.Close()\n\n\t\tok, err := registry.Default.WriteManual(path, m.Value, m.Version)\n\t\tif err != nil {\n\t\t\tlog.Print(\"[ERROR] \", err)\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif !ok {\n\t\t\thttp.Error(w, \"version mismatch\", http.StatusConflict)\n\t\t\treturn\n\t\t}\n\n\tdefault:\n\t\thttp.Error(w, \"not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n"
  },
  {
    "path": "admin/api/paths.go",
    "content": "package api\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/registry\"\n)\n\ntype ManualPathsHandler struct {\n\tPrefix string\n}\n\nfunc (h *ManualPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\t// we need this for testing.\n\t// under normal circumstances this is never nil\n\tif registry.Default == nil {\n\t\treturn\n\t}\n\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tpaths, err := registry.Default.ManualPaths()\n\t\tif err != nil {\n\t\t\tlog.Print(\"[ERROR] \", err)\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tfor i, p := range paths {\n\t\t\tpaths[i] = strings.TrimPrefix(p, h.Prefix)\n\t\t}\n\t\twriteJSON(w, r, paths)\n\t\treturn\n\n\tdefault:\n\t\thttp.Error(w, \"not allowed\", http.StatusMethodNotAllowed)\n\t}\n}\n"
  },
  {
    "path": "admin/api/routes.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/route\"\n)\n\ntype RoutesHandler struct{}\n\ntype apiRoute struct {\n\tService string   `json:\"service\"`\n\tHost    string   `json:\"host\"`\n\tPath    string   `json:\"path\"`\n\tSrc     string   `json:\"src\"`\n\tDst     string   `json:\"dst\"`\n\tOpts    string   `json:\"opts\"`\n\tCmd     string   `json:\"cmd\"`\n\tTags    []string `json:\"tags,omitempty\"`\n\tWeight  float64  `json:\"weight\"`\n\tRate1   float64  `json:\"rate1\"`\n\tPct99   float64  `json:\"pct99\"`\n}\n\nfunc (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tt := route.GetTable()\n\n\tif _, ok := r.URL.Query()[\"raw\"]; ok {\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tfmt.Fprintln(w, t.String())\n\t\treturn\n\t}\n\n\tvar hosts []string\n\tfor host := range t {\n\t\thosts = append(hosts, host)\n\t}\n\tsort.Strings(hosts)\n\n\tvar routes []apiRoute\n\tfor _, host := range hosts {\n\t\tfor _, tr := range t[host] {\n\t\t\tfor _, tg := range tr.Targets {\n\t\t\t\tvar opts []string\n\t\t\t\tfor k, v := range tg.Opts {\n\t\t\t\t\topts = append(opts, k+\"=\"+v)\n\t\t\t\t}\n\n\t\t\t\tar := apiRoute{\n\t\t\t\t\tService: tg.Service,\n\t\t\t\t\tHost:    tr.Host,\n\t\t\t\t\tPath:    tr.Path,\n\t\t\t\t\tSrc:     tr.Host + tr.Path,\n\t\t\t\t\tDst:     tg.URL.String(),\n\t\t\t\t\tOpts:    strings.Join(opts, \" \"),\n\t\t\t\t\tWeight:  tg.Weight,\n\t\t\t\t\tTags:    tg.Tags,\n\t\t\t\t\tCmd:     \"route add\",\n\t\t\t\t\t// Rate1:   tg.Timer.Rate1(),\n\t\t\t\t\t// Pct99:   tg.Timer.Percentile(0.99),\n\t\t\t\t}\n\t\t\t\troutes = append(routes, ar)\n\t\t\t}\n\t\t}\n\t}\n\twriteJSON(w, r, routes)\n}\n"
  },
  {
    "path": "admin/api/version.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype VersionHandler struct {\n\tVersion string\n}\n\nfunc (h *VersionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tfmt.Fprintf(w, \"%s\", h.Version)\n}\n"
  },
  {
    "path": "admin/server.go",
    "content": "package admin\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/admin/api\"\n\t\"github.com/fabiolb/fabio/admin/ui\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/proxy\"\n)\n\n// Server provides the HTTP server for the admin UI and API.\ntype Server struct {\n\tCfg      *config.Config\n\tAccess   string\n\tColor    string\n\tTitle    string\n\tVersion  string\n\tCommands string\n}\n\n// ListenAndServe starts the admin server.\nfunc (s *Server) ListenAndServe(l config.Listen, tlscfg *tls.Config) error {\n\treturn proxy.ListenAndServeHTTP(l, s.handler(), tlscfg)\n}\n\nfunc (s *Server) handler() http.Handler {\n\tmux := http.NewServeMux()\n\n\tswitch s.Access {\n\tcase \"ro\":\n\t\tmux.HandleFunc(\"/api/paths\", forbidden)\n\t\tmux.HandleFunc(\"/api/manual\", forbidden)\n\t\tmux.HandleFunc(\"/api/manual/\", forbidden)\n\t\tmux.HandleFunc(\"/manual\", forbidden)\n\t\tmux.HandleFunc(\"/manual/\", forbidden)\n\tcase \"rw\":\n\t\t// for historical reasons the configured config path starts with a '/'\n\t\t// but Consul treats all KV paths without a leading slash.\n\t\tpathsPrefix := strings.TrimPrefix(s.Cfg.Registry.Consul.KVPath, \"/\")\n\t\tmux.Handle(\"/api/paths\", &api.ManualPathsHandler{Prefix: pathsPrefix})\n\t\tmux.Handle(\"/api/manual\", &api.ManualHandler{BasePath: \"/api/manual\"})\n\t\tmux.Handle(\"/api/manual/\", &api.ManualHandler{BasePath: \"/api/manual\"})\n\t\tmux.Handle(\"/manual\", &ui.ManualHandler{\n\t\t\tBasePath: \"/manual\",\n\t\t\tColor:    s.Color,\n\t\t\tTitle:    s.Title,\n\t\t\tVersion:  s.Version,\n\t\t\tCommands: s.Commands,\n\t\t})\n\t\tmux.Handle(\"/manual/\", &ui.ManualHandler{\n\t\t\tBasePath: \"/manual\",\n\t\t\tColor:    s.Color,\n\t\t\tTitle:    s.Title,\n\t\t\tVersion:  s.Version,\n\t\t\tCommands: s.Commands,\n\t\t})\n\t}\n\n\tmux.Handle(\"/api/config\", &api.ConfigHandler{Config: s.Cfg})\n\tmux.Handle(\"/api/routes\", &api.RoutesHandler{})\n\tmux.Handle(\"/api/version\", &api.VersionHandler{Version: s.Version})\n\tmux.Handle(\"/routes\", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version, RoutingTable: s.Cfg.UI.RoutingTable})\n\tmux.HandleFunc(\"/health\", handleHealth)\n\n\tmux.Handle(\"/assets/\", http.FileServer(http.FS(ui.Static)))\n\tmux.HandleFunc(\"/favicon.ico\", http.NotFound)\n\n\tmux.Handle(\"/\", http.RedirectHandler(\"/routes\", http.StatusSeeOther))\n\treturn mux\n}\n\nfunc handleHealth(w http.ResponseWriter, r *http.Request) {\n\tfmt.Fprintln(w, \"OK\")\n}\n\nfunc forbidden(w http.ResponseWriter, r *http.Request) {\n\thttp.Error(w, \"Forbidden\", http.StatusForbidden)\n}\n"
  },
  {
    "path": "admin/server_test.go",
    "content": "package admin\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\nfunc TestAdminServerAccess(t *testing.T) {\n\ttype test struct {\n\t\turi  string\n\t\tcode int\n\t}\n\n\ttestAccess := func(access string, tests []test) {\n\t\tsrv := &Server{\n\t\t\tAccess: access,\n\t\t\tCfg: &config.Config{\n\t\t\t\tRegistry: config.Registry{\n\t\t\t\t\tConsul: config.Consul{\n\t\t\t\t\t\tKVPath: \"/fabio/config\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tts := httptest.NewServer(srv.handler())\n\t\tdefer ts.Close()\n\n\t\tnoRedirectClient := &http.Client{\n\t\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\t\treturn http.ErrUseLastResponse\n\t\t\t},\n\t\t}\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(access+tt.uri, func(t *testing.T) {\n\t\t\t\tresp, err := noRedirectClient.Get(ts.URL + tt.uri)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"got %v want nil\", err)\n\t\t\t\t}\n\t\t\t\tif got, want := resp.StatusCode, tt.code; got != want {\n\t\t\t\t\tt.Fatalf(\"got code %d want %d\", got, want)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\troTests := []test{\n\t\t{\"/api/manual\", 403},\n\t\t{\"/api/paths\", 403},\n\t\t{\"/api/config\", 200},\n\t\t{\"/api/routes\", 200},\n\t\t{\"/api/version\", 200},\n\t\t{\"/manual\", 403},\n\t\t{\"/routes\", 200},\n\t\t{\"/health\", 200},\n\t\t{\"/assets/logo.svg\", 200},\n\t\t{\"/assets/logo.bw.svg\", 200},\n\t\t{\"/\", 303},\n\t}\n\n\trwTests := []test{\n\t\t{\"/api/manual\", 200},\n\t\t{\"/api/paths\", 200},\n\t\t{\"/api/config\", 200},\n\t\t{\"/api/routes\", 200},\n\t\t{\"/api/version\", 200},\n\t\t{\"/manual\", 200},\n\t\t{\"/routes\", 200},\n\t\t{\"/health\", 200},\n\t\t{\"/assets/logo.svg\", 200},\n\t\t{\"/assets/logo.bw.svg\", 200},\n\t\t{\"/\", 303},\n\t}\n\n\ttestAccess(\"ro\", roTests)\n\ttestAccess(\"rw\", rwTests)\n}\n"
  },
  {
    "path": "admin/ui/assets/fonts/material-icons.css",
    "content": "@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: url(MaterialIcons-Regular.eot); /* For IE6-8 */\n  src: local('Material Icons'),\n       local('MaterialIcons-Regular'),\n       url(MaterialIcons-Regular.woff2) format('woff2'),\n       url(MaterialIcons-Regular.woff) format('woff'),\n       url(MaterialIcons-Regular.ttf) format('truetype');\n}\n\n.material-icons {\n  font-family: 'Material Icons';\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;  /* Preferred icon size */\n  display: inline-block;\n  line-height: 1;\n  text-transform: none;\n  letter-spacing: normal;\n  word-wrap: normal;\n  white-space: nowrap;\n  direction: ltr;\n\n  /* Support for all WebKit browsers. */\n  -webkit-font-smoothing: antialiased;\n  /* Support for Safari and Chrome. */\n  text-rendering: optimizeLegibility;\n\n  /* Support for Firefox. */\n  -moz-osx-font-smoothing: grayscale;\n\n  /* Support for IE. */\n  font-feature-settings: 'liga';\n}\n"
  },
  {
    "path": "admin/ui/generate.go",
    "content": "package ui\n\n//go:generate rm -rf assets/code.jquery.com\n//go:generate rm -rf assets/cdnjs.cloudflare.com\n//go:generate wget -pP assets https://code.jquery.com/jquery-3.6.0.min.js\n//go:generate wget -pP assets https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js\n//go:generate wget -pP assets https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css\n\n// https://google.github.io/material-design-icons/#setup-method-2-self-hosting\n//go:generate rm -rf assets/fonts\n//go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.ttf\n//go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.eot\n//go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.woff\n//go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.woff2\n//go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/material-icons.css\n"
  },
  {
    "path": "admin/ui/manual.go",
    "content": "package ui\n\nimport (\n\t\"html/template\"\n\t\"net/http\"\n)\n\ntype ManualHandler struct {\n\tBasePath string\n\tColor    string\n\tTitle    string\n\tVersion  string\n\tCommands string\n}\n\nfunc (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tpath := r.RequestURI[len(h.BasePath):]\n\tdata := struct {\n\t\t*ManualHandler\n\t\tPath    string\n\t\tAPIPath string\n\t}{\n\t\tManualHandler: h,\n\t\tPath:          path,\n\t\tAPIPath:       \"/api/manual\" + path,\n\t}\n\ttmplManual.ExecuteTemplate(w, \"manual\", data)\n}\n\nvar funcs = template.FuncMap{\n\t\"noescape\": func(str string) template.HTML {\n\t\treturn template.HTML(str)\n\t},\n}\n\nvar tmplManual = template.Must(template.New(\"manual\").Funcs(funcs).Parse( // language=HTML\n\t`<!doctype html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>fabio{{if .Title}} - {{.Title}}{{end}}</title>\n\t<script type=\"text/javascript\" src=\"/assets/code.jquery.com/jquery-3.6.0.min.js\"></script>\n\t<link href=\"/assets/fonts/material-icons.css\" rel=\"stylesheet\">\n\t<link rel=\"stylesheet\" href=\"/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css\">\n\t<script src=\"/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js\"></script>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n\n\t<style type=\"text/css\">\n\t\t.footer { padding-top: 10px; }\n\t\t.logo { height: 32px; margin: 0 auto; display: block; }\n\t</style>\n</head>\n<body>\n\n<ul id=\"overrides\" class=\"dropdown-content\"></ul>\n\n<nav class=\"top-nav {{.Color}}\">\n\n\t<div class=\"container\">\n\t\t<div class=\"nav-wrapper\">\n\t\t\t<a href=\"/\" class=\"brand-logo\"><img alt=\"Fabio Logo\" style=\"margin: 15px 0\" class=\"logo\" src=\"/assets/logo.bw.svg\"> {{if .Title}} - {{.Title}}{{end}}</a>\n\t\t\t<ul id=\"nav-mobile\" class=\"right hide-on-med-and-down\">\n\t\t\t\t<li><a href=\"/routes\">Routes</a></li>\n\t\t\t\t<li><a class=\"dropdown-trigger dropdown-button\" href=\"#\" data-target=\"overrides\">Overrides<i class=\"material-icons right\">arrow_drop_down</i></a></li>\n\t\t\t\t<li><a href=\"https://github.com/fabiolb/fabio/blob/master/CHANGELOG.md\">{{.Version}}</a></li>\n\t\t\t\t<li><a href=\"https://github.com/fabiolb/fabio\">Github</a></li>\n\t\t\t\t<li><a href=\"https://fabiolb.net\">Fabiolb.net</a></li>\n\t\t\t</ul>\n\t\t</div>\n\t</div>\n\n</nav>\n\n<div class=\"container\">\n\n\t<div class=\"section\">\n\t\t<h5>Manual Routes{{if .Path}} for \"{{.Path}}\"{{end}}</h5>\n\n\t\t<div class=\"row\">\n\t\t\t<form class=\"col s12\">\n\t\t\t\t<input type=\"hidden\" name=\"version\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"input-field col s12\">\n\t\t\t\t\t\t<textarea id=\"textarea1\" class=\"materialize-textarea\"></textarea>\n\t\t\t\t\t\t<label for=\"textarea1\"></label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t\t<button class=\"btn waves-effect waves-light\" name=\"save\">Save</button>\n\t\t\t<button class=\"btn waves-effect waves-light\" name=\"help\">Help</button>\n\t\t</div>\n\n\t\t<div class=\"row\">\n\t\t\t<pre class=\"help hide\">{{.Commands}}</pre>\n\t\t</div>\n\t</div>\n\n</div>\n\n<script>\n$(function(){\n\t$('.dropdown-trigger').dropdown();\n\tlet params={};window.location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(str,key,value){params[key] = value;});\n\n\t$.get({{.APIPath}}, function(data) {\n\t\tconst $ta1 = $(\"#textarea1\");\n\t\t$(\"input[name=version]\").val(data.version);\n\t\t$(\"textarea>label\").val(\"Version \" + data.version);\n\t\t$ta1.val(data.value);\n\t\tM.textareaAutoResize($('#textarea1'));\n\t});\n\n\t$.get('/api/paths', function(data) {\n\t\tconst d = $(\"#overrides\");\n\t\t$.each(data, function(idx, val) {\n\t\t\tlet path = val;\n\t\t\tif (val == \"\") {\n\t\t\t\tval = \"default\";\n\t\t\t}\n\t\t\td.append(\n\t\t\t\t$('<li />').append(\n\t\t\t\t\t$('<a />').attr('href', '/manual'+path).text(val)\n\t\t\t\t)\n\t\t\t);\n\t\t});\n\t});\n\n\t$(\"button[name=help]\").click(function() {\n\t\t$(\"pre.help\").toggleClass(\"hide\");\n\t});\n\n\t$(\"button[name=save]\").click(function() {\n\t\tconst data = {\n\t\t\tvalue   : $(\"#textarea1\").val(),\n\t\t\tversion : $(\"input[name=version]\").val()\n\t\t}\n\t\t$.ajax({{.APIPath}}, {\n\t\t\ttype: 'PUT',\n\t\t\tdata: JSON.stringify(data),\n\t\t\tcontentType: 'application/json',\n\t\t\tstatusCode: {\n\t\t\t\t400: function(jqXHR, textStatus, err) { alert(err); },\n\t\t\t\t409: function(jqXHR, textStatus, err) { alert(err); },\n\t\t\t\t500: function(jqXHR, textStatus, err) { alert(err); }\n\t\t\t},\n\t\t\tsuccess: function() {\n\t\t\t\twindow.location.reload();\n\t\t\t}\n\t\t});\n\t});\n})\n</script>\n\n</body>\n</html>\n`))\n"
  },
  {
    "path": "admin/ui/route.go",
    "content": "package ui\n\nimport (\n\t\"html/template\"\n\t\"net/http\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\n// RoutesHandler provides the UI for managing the routing table.\ntype RoutesHandler struct {\n\tColor        string\n\tTitle        string\n\tVersion      string\n\tRoutingTable config.RoutingTable\n}\n\nfunc (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {\n\ttmplRoutes.ExecuteTemplate(w, \"routes\", h)\n}\n\nvar tmplRoutes = template.Must(template.New(\"routes\").Parse( // language=HTML\n\t`<!doctype html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<title>fabio{{if .Title}} - {{.Title}}{{end}}</title>\n\t<script type=\"text/javascript\" src=\"/assets/code.jquery.com/jquery-3.6.0.min.js\"></script>\n\t<link href=\"/assets/fonts/material-icons.css\" rel=\"stylesheet\">\n\t<link rel=\"stylesheet\" href=\"/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css\">\n\t<script src=\"/assets/cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js\"></script>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n\n\t<style type=\"text/css\">\n\t\ttd.tags { display: none; }\n\t\t.footer { padding-top: 10px; }\n\t\t.logo { height: 32px; margin: 0 auto; display: block; }\n\n\t\t@media (min-width: 78em) {\n\t\t\ttd.tags{ display: table-cell; }\n\t\t}\n\n\t\t.tooltip { position: relative; display: inline-block; }\n\t\t.tooltip .tooltiptext {\n\t\t\tvisibility: hidden;\n\t\t\twidth: 250px;\n\t\t\tbackground-color: #000000;\n\t\t\tcolor: #ffffff;\n\t\t\ttext-align: center;\n\t\t\tmargin: 30px 0px 0px 30px;\n\t\t\tpadding: 5px 0;\n\t\t\tborder-radius: 6px;\n\t\t\tposition: absolute;\n\t\t\tz-index: 1000;\n\t\t}\n\t\t.tooltip:hover .tooltiptext { visibility: visible; }\n\t</style>\n</head>\n<body>\n\n<ul id=\"overrides\" class=\"dropdown-content\"></ul>\n\n<nav class=\"top-nav {{.Color}}\">\n\n\t<div class=\"container\">\n\t\t<div class=\"nav-wrapper\">\n\t\t\t<a href=\"/\" class=\"brand-logo\"><img alt=\"Fabio Logo\" style=\"margin: 15px 0\" class=\"logo\" src=\"/assets/logo.bw.svg\"> {{if .Title}} - {{.Title}}{{end}}</a>\n\t\t\t<ul id=\"nav-mobile\" class=\"right hide-on-med-and-down\">\n\t\t\t\t<li><a class=\"dropdown-trigger dropdown-button\" href=\"#\" data-target=\"overrides\">Overrides<i class=\"material-icons right\">arrow_drop_down</i></a></li>\n\t\t\t\t<li><a href=\"https://github.com/fabiolb/fabio/blob/master/CHANGELOG.md\">{{.Version}}</a></li>\n\t\t\t\t<li><a href=\"https://github.com/fabiolb/fabio\">Github</a></li>\n\t\t\t\t<li><a href=\"https://fabiolb.net\">Fabiolb.net</a></li>\n\t\t\t</ul>\n\t\t</div>\n\t</div>\n\n</nav>\n\n<div class=\"container\">\n\n\t<div class=\"section\">\n\t\t<h5>Routing Table</h5>\n\t\t<p><input type=\"text\" id=\"filter\" placeholder=\"type to filter routes\"></p>\n\t\t<table class=\"routes highlight\"></table>\n\t</div>\n\n\t<div class=\"section footer\">\n\t\t<img alt=\"Fabio Logo\" class=\"logo\" src=\"/assets/logo.svg\">\n\t</div>\n\n</div>\n\n<script>\n$(function(){\n\tlet params={};window.location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(str,key,value){params[key] = value;});\n\n\t$('.dropdown-trigger').dropdown();\n\tfunction renderRoutes(routes) {\n\t\tconst $table = $('table.routes');\n\n\t\tlet thead = '<thead><tr>';\n\t\tthead += '<th>#</th>';\n\t\tthead += '<th>Service</th>';\n\t\tthead += '<th>Source</th>';\n\t\tthead += '<th>Dest</th>';\n\t\tthead += '<th>Options</th>';\n\t\tthead += '<th>Weight</th>';\n\t\tthead += '</tr></thead>';\n\n\t\tlet $tbody = $('<tbody />');\n\n\t\tconsole.log(routes);\n\n\t\tif (routes != null) {\n\t\t\tfor (let i=0; i < routes.length; i++) {\n\t\t\t\tconst r = routes[i];\n\t\t\t\t\n\t\t\t\tlet $tr = $('<tr />');\n\t\t\t\tif (/^https?:\\/+/i.exec(r.src) != null) { \n\t\t\t\t\t$tr = $('<tr />').attr('style', 'background-color: #ff1a1a;');\n\t\t\t\t\t$tr.append($('<td />').addClass('tooltip').append($('<span class=\"tooltiptext\"></span>').text(\"Route Source cannot start with the protocol or scheme (e.g. - 'http' and 'https' are invalid to have listed in the route source)\")).append($('<span class=\"valign-wrapper\" />').append(i+1).append($('<i class=\"material-icons\">error_outline</i>'))));\n\t\t\t\t} else {\n\t\t\t\t\t$tr.append($('<td />').text(i+1));\n\t\t\t\t}\n\t\t\t\t$tr.append($('<td />').text(r.service));\n\n\t\t\t\tif ({{.RoutingTable.Source.LinkEnabled}} == true && /^https?:\\/+/i.exec(r.dst) != null && /^https?:\\/+/i.exec(r.src) == null) {\n\t\t\t\t\tconst hrefScheme = ({{.RoutingTable.Source.Scheme}} != '' ? {{.RoutingTable.Source.Scheme}} + ':' : window.location.protocol) + '//';\n\t\t\t\t\tconst hrefHost = ({{.RoutingTable.Source.Host}} != '' ? {{.RoutingTable.Source.Host}} : window.location.hostname);\n\t\t\t\t\tconst hrefPort = (/:/gi.exec(r.src) != null ? /:[0-9]*\\/?/gi.exec(r.src)[0] : '{{if .RoutingTable.Source.Port}}:{{.RoutingTable.Source.Port}}{{end}}');\n\t\t\t\t\tconst hrefStr = (r.src.startsWith('/') ? hrefScheme + hrefHost + hrefPort : '{{if .RoutingTable.Source.Scheme}}{{.RoutingTable.Source.Scheme}}:{{end}}//') + r.src;\n\t\t\t\t\t$tr.append($('<td />').append($('<a />').attr('href', hrefStr){{if .RoutingTable.Source.NewTab}}.attr('target', '_blank'){{end}}.text(r.src)));\n\t\t\t\t} else {\n\t\t\t\t\t$tr.append($('<td />').text(r.src));\n\t\t\t\t}\n\n\t\t\t\t$tr.append($('<td />').append($('<a />').attr('href', r.dst).text(r.dst)));\n\t\t\t\t$tr.append($('<td />').text(r.opts));\n\t\t\t\t$tr.append($('<td />').text((r.weight * 100).toFixed(2) + '%'));\n\n\t\t\t\t$tr.appendTo($tbody);\n\t\t\t}\n\t\t}\n\n\t\t$table.empty().\n\t\t\tappend($(thead)).\n\t\t\tappend($tbody);\n\t}\n\n\tlet $filter = $('#filter');\n\tfunction doFilter(v) {\n\t\t$(\"tr\").show();\n\t\tif (!v) return;\n\t\tlet words = v.split(' ');\n\t\tfor (let i=0; i < words.length; i++) {\n\t\t\tlet w = words[i].trim();\n\t\t\tif (w === \"\") continue;\n\t\t\t$(\"tbody tr:not(:contains('\"+w+\"'))\").hide();\n\t\t}\n\t}\n\n\t$filter.focus();\n\t$filter.keyup(function() {\n\t\tconst v = $filter.val();\n\t\twindow.history.pushState(null, null, \"?filter=\" +v);\n\t\tdoFilter(v);\n\t});\n\n\t$.get(\"/api/routes\", function(data) {\n\t\trenderRoutes(data);\n\t\tif (!params.filter) return;\n\t\tconst v = decodeURIComponent(params.filter);\n\t\t$filter.val(v);\n\t\tdoFilter(v);\n\t});\n\n\t$.get('/api/paths', function(data) {\n\t\tconst d = $(\"#overrides\");\n\t\t$.each(data, function(idx, val) {\n\t\t\tlet path = val;\n\t\t\tif (val == \"\") {\n\t\t\t\tval = \"default\";\n\t\t\t}\n\t\t\td.append(\n\t\t\t\t$('<li />').append(\n\t\t\t\t\t$('<a />').attr('href', '/manual'+path).text(val)\n\t\t\t\t)\n\t\t\t);\n\t\t});\n\t});\n});\n</script>\n\n</body>\n</html>\n`))\n"
  },
  {
    "path": "admin/ui/static.go",
    "content": "package ui\n\nimport \"embed\"\n\n//go:embed assets/*\nvar Static embed.FS\n"
  },
  {
    "path": "assert/assert.go",
    "content": "// Package assert provides a simple assert framework.\npackage assert\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n)\n\n// Equal provides an assertEqual function\nfunc Equal(t *testing.T) func(got, want interface{}) {\n\treturn EqualDepth(t, 1, \"\")\n}\n\nfunc EqualDepth(t *testing.T, calldepth int, desc string) func(got, want interface{}) {\n\treturn func(got, want interface{}) {\n\t\t_, file, line, _ := runtime.Caller(calldepth)\n\t\tif !reflect.DeepEqual(got, want) {\n\t\t\tfmt.Printf(\"\\t%s:%d: %s: got %v want %v\\n\", filepath.Base(file), line, desc, got, want)\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\ntype AuthScheme interface {\n\tAuthorized(request *http.Request, response http.ResponseWriter) bool\n}\n\nfunc LoadAuthSchemes(cfg map[string]config.AuthScheme) (map[string]AuthScheme, error) {\n\tauths := map[string]AuthScheme{}\n\tfor _, a := range cfg {\n\t\tswitch a.Type {\n\t\tcase \"basic\":\n\t\t\tb, err := newBasicAuth(a.Basic)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tauths[a.Name] = b\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown auth type '%s'\", a.Type)\n\t\t}\n\t}\n\n\treturn auths, nil\n}\n"
  },
  {
    "path": "auth/auth_test.go",
    "content": "package auth\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\nfunc TestLoadAuthSchemes(t *testing.T) {\n\n\tt.Run(\"should fail when auth scheme fails to load\", func(t *testing.T) {\n\t\t_, err := LoadAuthSchemes(map[string]config.AuthScheme{\n\t\t\t\"myauth\": {\n\t\t\t\tName: \"myauth\",\n\t\t\t\tType: \"basic\",\n\t\t\t\tBasic: config.BasicAuth{\n\t\t\t\t\tFile: \"/some/non/existent/file\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tconst errorText = \"open /some/non/existent/file: no such file or directory\"\n\n\t\tif err.Error() != errorText {\n\t\t\tt.Fatalf(\"got %s, want %s\", err.Error(), errorText)\n\t\t}\n\t})\n\n\tt.Run(\"should return an error when auth type is unknown\", func(t *testing.T) {\n\t\t_, err := LoadAuthSchemes(map[string]config.AuthScheme{\n\t\t\t\"myauth\": {\n\t\t\t\tName: \"myauth\",\n\t\t\t\tType: \"foo\",\n\t\t\t},\n\t\t})\n\n\t\tconst errorText = \"unknown auth type 'foo'\"\n\n\t\tif err.Error() != errorText {\n\t\t\tt.Fatalf(\"got %s, want %s\", err.Error(), errorText)\n\t\t}\n\t})\n\n\tt.Run(\"should load multiple auth schemes\", func(t *testing.T) {\n\t\tmyauth, err := createBasicAuthFile(\"foo:bar\", t)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"could not create file on disk %s\", err)\n\t\t}\n\n\t\tmyotherauth, err := createBasicAuthFile(\"bar:foo\", t)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"could not create file on disk %s\", err)\n\t\t}\n\n\t\tresult, _ := LoadAuthSchemes(map[string]config.AuthScheme{\n\t\t\t\"myauth\": {\n\t\t\t\tName: \"myauth\",\n\t\t\t\tType: \"basic\",\n\t\t\t\tBasic: config.BasicAuth{\n\t\t\t\t\tFile: myauth,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"myotherauth\": {\n\t\t\t\tName: \"myotherauth\",\n\t\t\t\tType: \"basic\",\n\t\t\t\tBasic: config.BasicAuth{\n\t\t\t\t\tFile: myotherauth,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tif len(result) != 2 {\n\t\t\tt.Fatalf(\"expected 2 auth schemes, got %d\", len(result))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "auth/basic.go",
    "content": "package auth\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/tg123/go-htpasswd\"\n)\n\n// basic is an implementation of AuthScheme\ntype basic struct {\n\tsecrets *htpasswd.File\n\trealm   string\n}\n\nfunc newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) {\n\tbad := func(err error) {\n\t\tlog.Println(\"[WARN] Error processing a line in an htpasswd file:\", err)\n\t}\n\n\tsecrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, bad)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cfg.Refresh > 0 {\n\t\tstat, err := os.Stat(cfg.File)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcfg.ModTime = stat.ModTime()\n\n\t\tgo func() {\n\t\t\tcleared := false\n\t\t\tticker := time.NewTicker(cfg.Refresh).C\n\t\t\tfor range ticker {\n\t\t\t\tstat, err := os.Stat(cfg.File)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"[WARN] Error accessing htpasswd file:\", err)\n\t\t\t\t\tif !cleared {\n\t\t\t\t\t\terr = secrets.ReloadFromReader(&bytes.Buffer{}, bad)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Println(\"[WARN] Error clearing the htpasswd credentials:\", err)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.Println(\"[INFO] The htpasswd credentials have been cleared\")\n\t\t\t\t\t\t\tcleared = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// refresh the htpasswd file only if its modification time has changed\n\t\t\t\t// even if the new htpasswd file is older than previously loaded\n\t\t\t\tif cfg.ModTime != stat.ModTime() {\n\t\t\t\t\tif err := secrets.Reload(bad); err == nil {\n\t\t\t\t\t\tlog.Println(\"[INFO] The htpasswd file has been successfully reloaded\")\n\t\t\t\t\t\tcfg.ModTime = stat.ModTime()\n\t\t\t\t\t\tcleared = false\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Println(\"[WARN] Error reloading htpasswd file:\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\treturn &basic{\n\t\tsecrets: secrets,\n\t\trealm:   cfg.Realm,\n\t}, nil\n}\n\nfunc (b *basic) Authorized(request *http.Request, response http.ResponseWriter) bool {\n\tuser, password, ok := request.BasicAuth()\n\n\tif !ok {\n\t\tresponse.Header().Set(\"WWW-Authenticate\", \"Basic realm=\\\"\"+b.realm+\"\\\"\")\n\t\treturn false\n\t}\n\n\treturn b.secrets.Match(user, password)\n}\n"
  },
  {
    "path": "auth/basic_test.go",
    "content": "package auth\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/uuid\"\n)\n\ntype responseWriter struct {\n\theader  http.Header\n\tcode    int\n\twritten []byte\n}\n\nfunc (rw *responseWriter) Header() http.Header {\n\tif rw.header == nil {\n\t\trw.header = map[string][]string{}\n\t}\n\treturn rw.header\n}\n\nfunc (rw *responseWriter) Write(b []byte) (int, error) {\n\trw.written = append(rw.written, b...)\n\treturn len(rw.written), nil\n}\n\nfunc (rw *responseWriter) WriteHeader(statusCode int) {\n\trw.code = statusCode\n}\n\nfunc createBasicAuthFile(contents string, t *testing.T) (string, error) {\n\tdir := t.TempDir()\n\n\tfilename := fmt.Sprintf(\"%s/%s\", dir, uuid.NewUUID())\n\n\terr := os.WriteFile(filename, []byte(contents), 0666)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not write password file: %s\", err)\n\t}\n\n\treturn filename, nil\n}\n\nfunc createBasicAuth(user string, password string, t *testing.T) (AuthScheme, error) {\n\tcontents := fmt.Sprintf(\"%s:%s\", user, password)\n\n\tfilename, err := createBasicAuthFile(contents, t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create basic auth: %s\", err)\n\t}\n\n\ta, err := newBasicAuth(config.BasicAuth{\n\t\tFile:  filename,\n\t\tRealm: \"testrealm\",\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create basic auth: %s\", err)\n\t}\n\n\treturn a, nil\n}\n\nfunc TestNewBasicAuth(t *testing.T) {\n\n\tt.Run(\"should create a basic auth scheme from the supplied config\", func(t *testing.T) {\n\t\tfilename, err := createBasicAuthFile(\"foo:bar\", t)\n\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\t_, err = newBasicAuth(config.BasicAuth{\n\t\t\tFile: filename,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t})\n\n\tt.Run(\"should log a warning when credentials are malformed\", func(t *testing.T) {\n\t\tfilename, err := createBasicAuthFile(\"foosdlijdgohdgdbar\", t)\n\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\t_, err = newBasicAuth(config.BasicAuth{\n\t\t\tFile: filename,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t})\n}\n\nfunc TestBasic_Authorised(t *testing.T) {\n\tbasicAuth, err := createBasicAuth(\"foo\", \"bar\", t)\n\tcreds := []byte(\"foo:bar\")\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\treq  *http.Request\n\t\tres  http.ResponseWriter\n\t\tout  bool\n\t}{\n\t\t{\n\t\t\t\"correct credentials should be authorized\",\n\t\t\t&http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Authorization\": []string{fmt.Sprintf(\"Basic %s\", base64.StdEncoding.EncodeToString(creds))},\n\t\t\t\t},\n\t\t\t},\n\t\t\t&responseWriter{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"incorrect credentials should not be authorized\",\n\t\t\t&http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Authorization\": []string{fmt.Sprintf(\"Basic %s\", base64.StdEncoding.EncodeToString([]byte(\"baz:blarg\")))},\n\t\t\t\t},\n\t\t\t},\n\t\t\t&responseWriter{},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"missing Authorization header should not be authorized\",\n\t\t\t&http.Request{\n\t\t\t\tHeader: http.Header{},\n\t\t\t},\n\t\t\t&responseWriter{},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"malformed Authorization header should not be authorized\",\n\t\t\t&http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Authorization\": []string{\"malformed\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\t&responseWriter{},\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got, want := basicAuth.Authorized(tt.req, tt.res), tt.out; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBasic_Authorised_should_fail_without_htpasswd_file(t *testing.T) {\n\tfilename, err := createBasicAuthFile(\"foo:bar\", t)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\ta, err := newBasicAuth(config.BasicAuth{\n\t\tFile:    filename,\n\t\tRefresh: time.Second,\n\t})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tcreds := []byte(\"foo:bar\")\n\tr := &http.Request{\n\t\tHeader: http.Header{\n\t\t\t\"Authorization\": []string{fmt.Sprintf(\"Basic %s\", base64.StdEncoding.EncodeToString(creds))},\n\t\t},\n\t}\n\n\tw := &responseWriter{}\n\n\tt.Run(\"should authorize against supplied htpasswd file\", func(t *testing.T) {\n\t\tif got, want := a.Authorized(r, w), true; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t}\n\t})\n\n\tif err := os.Remove(filename); err != nil {\n\t\tt.Fatalf(\"removing htpasswd file: %s\", err)\n\t}\n\n\ttime.Sleep(2 * time.Second) // ensure htpasswd file refresh happened\n\n\tt.Run(\"should not authorize after removing htpasswd file\", func(t *testing.T) {\n\t\tif got, want := a.Authorized(r, w), false; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t}\n\t})\n}\n\nfunc TestBasic_Authorized_should_set_www_realm_header(t *testing.T) {\n\tbasicAuth, err := createBasicAuth(\"foo\", \"bar\", t)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trw := &responseWriter{}\n\n\t_ = basicAuth.Authorized(&http.Request{Header: http.Header{}}, rw)\n\n\tgot := rw.Header().Get(\"WWW-Authenticate\")\n\twant := `Basic realm=\"testrealm\"`\n\n\tif strings.Compare(got, want) != 0 {\n\t\tt.Errorf(\"got '%s', want '%s'\", got, want)\n\t}\n}\n"
  },
  {
    "path": "bgp/bgp_nonwindows.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage bgp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/exit\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/protobuf/proto\"\n\tapb \"google.golang.org/protobuf/types/known/anypb\"\n\n\tapi \"github.com/osrg/gobgp/v3/api\"\n\tbgpconfig \"github.com/osrg/gobgp/v3/pkg/config\"\n\t\"github.com/osrg/gobgp/v3/pkg/server\"\n)\n\nvar (\n\tErrMissingAnycast  = errors.New(\"you must specify at least one anycast address to advertise\")\n\tErrMissingPeers    = errors.New(\"you must specify at least one peer to advertise routes to\")\n\tErrMissingRouterID = errors.New(\"you must specify the routerID of this host, i.e. a non anycast address\")\n\tErrNoRoutesAdded   = errors.New(\"no routes were successfully added\")\n\tErrNoRoutesDeleted = errors.New(\"no routes were successfully deleted\")\n\tErrNoPeersAdded    = errors.New(\"no peers were successfully added\")\n)\n\nconst (\n\tdenyAllNeighbors = \"deny-all-neighbors\"\n\tmatchAnyPeer     = \"match-any-peer\"\n\tglobalTable      = \"global\"\n\trejectAll        = \"reject-all\"\n)\n\ntype BGPHandler struct {\n\tserver     *server.BgpServer\n\tconfig     *config.BGP\n\trouteAttrs []*apb.Any\n}\n\nfunc NewBGPHandler(config *config.BGP) (*BGPHandler, error) {\n\t// pre-chew some protobuf messages that are part of\n\t// every anycast route we'll be adding.\n\tnextHop := config.RouterID\n\tif len(config.NextHop) > 0 {\n\t\tnextHop = config.NextHop\n\t}\n\tvar messages = []proto.Message{\n\t\t&api.OriginAttribute{\n\t\t\tOrigin: 0,\n\t\t},\n\t\t&api.NextHopAttribute{\n\t\t\tNextHop: nextHop,\n\t\t},\n\t\t&api.AsPathAttribute{\n\t\t\tSegments: []*api.AsSegment{\n\t\t\t\t{\n\t\t\t\t\tType:    api.AsSegment_AS_SEQUENCE,\n\t\t\t\t\tNumbers: []uint32{uint32(config.Asn)},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tattributes := make([]*apb.Any, 0, len(messages))\n\tfor _, p := range messages {\n\t\tattr, err := apb.New(p)\n\t\tif err != nil {\n\t\t\t// should never happen\n\t\t\tpanic(err)\n\t\t}\n\t\tattributes = append(attributes, attr)\n\t}\n\n\tvar opts = []server.ServerOption{server.LoggerOption(bgpLogger{})}\n\tif config.EnableGRPC {\n\t\tmaxSize := 256 << 20\n\t\tgrpcOpts := []grpc.ServerOption{grpc.MaxRecvMsgSize(maxSize), grpc.MaxSendMsgSize(maxSize)}\n\t\tif config.GRPCTLS {\n\t\t\tcreds, err := credentials.NewServerTLSFromFile(config.CertFile, config.KeyFile)\n\t\t\tif err != nil {\n\t\t\t\t// shouldn't get here if validate was called first.\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing bgp TLS credentials: %s\", err)\n\t\t\t}\n\t\t\tgrpcOpts = append(grpcOpts, grpc.Creds(creds))\n\t\t}\n\t\topts = append(opts,\n\t\t\tserver.GrpcOption(grpcOpts),\n\t\t\tserver.GrpcListenAddress(config.GRPCListenAddress),\n\t\t)\n\t}\n\treturn &BGPHandler{\n\t\tserver:     server.NewBgpServer(opts...),\n\t\tconfig:     config,\n\t\trouteAttrs: attributes,\n\t}, nil\n}\n\nfunc (bgph *BGPHandler) Start() error {\n\ts := bgph.server\n\tgo s.Serve()\n\n\tif len(bgph.config.GOBGPDCfgFile) > 0 {\n\t\tinitialCfg, err := bgpconfig.ReadConfigFile(bgph.config.GOBGPDCfgFile, \"toml\")\n\t\tif err != nil {\n\t\t\t// shouldn't happen if we called validate first.\n\t\t\treturn err\n\t\t}\n\t\t_, err = bgpconfig.InitialConfig(context.Background(), s, initialCfg, false)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bgp: error initializing from gobgp config: %w\", err)\n\t\t}\n\t} else {\n\t\t// If we weren't passed a gobgp config file, configure using the values passed from the fabio\n\t\t// config, and make sure we have a sane policy where we export our routes to peers but don't\n\t\t// import from any peers.\n\t\terr := bgph.startBGP(context.Background())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bgp: error starting: %w\", err)\n\t\t}\n\n\t\terr = bgph.setPolicies()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bgp error setting policy: %w\", err)\n\t\t}\n\n\t}\n\n\terrCh := make(chan error, 1)\n\texit.Listen(func(sig os.Signal) {\n\t\tlog.Printf(\"[INFO] Stopping BGP\")\n\t\terr := s.StopBgp(context.Background(), &api.StopBgpRequest{})\n\t\terrCh <- err\n\t})\n\n\t// monitor the change of the peer state\n\tif err := s.WatchEvent(context.Background(), &api.WatchEventRequest{Peer: &api.WatchEventRequest_Peer{}}, func(r *api.WatchEventResponse) {\n\t\tif p := r.GetPeer(); p != nil && p.Type == api.WatchEventResponse_PeerEvent_STATE {\n\t\t\tlog.Printf(\"[DEBUG] bgp event: %#v\", p)\n\t\t}\n\t}); err != nil {\n\t\tlog.Printf(\"[ERROR] bgp watcher failed: %s\", err)\n\t}\n\tif len(bgph.config.GOBGPDCfgFile) == 0 || len(bgph.config.Peers) > 0 {\n\t\t// add peers\n\t\terr := bgph.addNeighbors(context.Background(), bgph.config.Peers)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bgp error adding neighbors: %w\", err)\n\t\t}\n\t}\n\tif len(bgph.config.AnycastAddresses) > 0 {\n\t\terr := bgph.AddRoutes(context.Background(), bgph.config.AnycastAddresses)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bgp error adding anycastaddresses: %w\", err)\n\t\t}\n\t}\n\t// hang until exit handler completes above.\n\treturn <-errCh\n}\n\nfunc (bgph *BGPHandler) startBGP(ctx context.Context) error {\n\treturn bgph.server.StartBgp(ctx, &api.StartBgpRequest{\n\t\tGlobal: &api.Global{\n\t\t\tAsn:             uint32(bgph.config.Asn),\n\t\t\tRouterId:        bgph.config.RouterID,\n\t\t\tListenPort:      int32(bgph.config.ListenPort),\n\t\t\tListenAddresses: bgph.config.ListenAddresses,\n\t\t},\n\t})\n}\n\nfunc (bgph *BGPHandler) setPolicies() error {\n\t// Create a policy that denies all routes from any neighbor.\n\terr := bgph.server.SetPolicies(context.Background(), &api.SetPoliciesRequest{\n\t\tDefinedSets: []*api.DefinedSet{\n\t\t\t{\n\t\t\t\tDefinedType: api.DefinedType_NEIGHBOR,\n\t\t\t\tName:        matchAnyPeer,\n\t\t\t\tList:        []string{\"0.0.0.0/0\", \"::/0\"},\n\t\t\t},\n\t\t},\n\t\tPolicies: []*api.Policy{\n\t\t\t{\n\t\t\t\tName: denyAllNeighbors,\n\t\t\t\tStatements: []*api.Statement{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: rejectAll,\n\t\t\t\t\t\tConditions: &api.Conditions{\n\t\t\t\t\t\t\tNeighborSet: &api.MatchSet{\n\t\t\t\t\t\t\t\tName: matchAnyPeer,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tActions: &api.Actions{\n\t\t\t\t\t\t\tRouteAction: api.RouteAction_REJECT,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Assign the above to the global policy\n\treturn bgph.server.SetPolicyAssignment(context.Background(), &api.SetPolicyAssignmentRequest{\n\t\tAssignment: &api.PolicyAssignment{\n\t\t\tName:      globalTable, // this is the global rib\n\t\t\tDirection: api.PolicyDirection_IMPORT,\n\t\t\tPolicies: []*api.Policy{\n\t\t\t\t{\n\t\t\t\t\tName: denyAllNeighbors,\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Need to set default action to accept here because otherwise\n\t\t\t// even routes added via API calls get rejected.\n\t\t\tDefaultAction: api.RouteAction_ACCEPT,\n\t\t},\n\t})\n}\n\nfunc (bgph *BGPHandler) addNeighbors(ctx context.Context, peers []config.BGPPeer) error {\n\tvar errs []error\n\tpeerCount := 0\n\tfor _, peer := range peers {\n\t\tvar hop *api.EbgpMultihop\n\t\tif peer.MultiHop {\n\t\t\thop = &api.EbgpMultihop{\n\t\t\t\tEnabled:     true,\n\t\t\t\tMultihopTtl: uint32(peer.MultiHopLength),\n\t\t\t}\n\t\t}\n\t\tvar trans *api.Transport\n\t\tif peer.NeighborPort > 0 {\n\t\t\ttrans = &api.Transport{\n\t\t\t\tLocalAddress:  bgph.config.RouterID,\n\t\t\t\tMtuDiscovery:  false,\n\t\t\t\tPassiveMode:   false,\n\t\t\t\tRemoteAddress: peer.NeighborAddress,\n\t\t\t\tRemotePort:    uint32(peer.NeighborPort),\n\t\t\t\tTcpMss:        0,\n\t\t\t\tBindInterface: \"\",\n\t\t\t}\n\t\t}\n\t\terr := bgph.server.AddPeer(ctx, &api.AddPeerRequest{\n\t\t\tPeer: &api.Peer{\n\t\t\t\tConf: &api.PeerConf{\n\t\t\t\t\tAuthPassword:    peer.Password,\n\t\t\t\t\tNeighborAddress: peer.NeighborAddress,\n\t\t\t\t\tPeerAsn:         uint32(peer.Asn),\n\t\t\t\t},\n\t\t\t\tEbgpMultihop: hop,\n\t\t\t\tTransport:    trans,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tpeerCount++\n\t}\n\tif peerCount == 0 {\n\t\terrs = append(errs, ErrNoPeersAdded)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc (bgph *BGPHandler) AddRoutes(ctx context.Context, routes []string) error {\n\tvar errs []error\n\t// Add our Anycast routes\n\n\troutesAdded := 0\n\n\tfor _, addr := range routes {\n\t\t_, ipnet, err := net.ParseCIDR(addr)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tprefixLen, _ := ipnet.Mask.Size()\n\t\taf := api.Family_AFI_IP\n\t\tif ipnet.IP.To4() == nil {\n\t\t\taf = api.Family_AFI_IP6\n\t\t}\n\t\tnlri, _ := apb.New(&api.IPAddressPrefix{\n\t\t\tPrefixLen: uint32(prefixLen),\n\t\t\tPrefix:    ipnet.IP.String(),\n\t\t})\n\t\t_, err = bgph.server.AddPath(ctx, &api.AddPathRequest{\n\t\t\tPath: &api.Path{\n\t\t\t\tNlri:   nlri,\n\t\t\t\tPattrs: bgph.routeAttrs,\n\t\t\t\tFamily: &api.Family{\n\t\t\t\t\tAfi:  af,\n\t\t\t\t\tSafi: api.Family_SAFI_UNICAST,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] bgp error adding path for %s: %s\", addr, err)\n\t\t\terrs = append(errs, fmt.Errorf(\"error adding %s: %w\", addr, err))\n\t\t} else {\n\t\t\tlog.Printf(\"[INFO] bgp successfully added path for %s\", addr)\n\t\t\troutesAdded++\n\t\t}\n\t}\n\tif routesAdded == 0 {\n\t\terrs = append(errs, ErrNoRoutesAdded)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc (bgph *BGPHandler) DeleteRoutes(ctx context.Context, routes []string) error {\n\tvar errs []error\n\tdelCount := 0\n\tfor _, addr := range routes {\n\t\t_, ipnet, err := net.ParseCIDR(addr)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tprefixLen, _ := ipnet.Mask.Size()\n\t\taf := api.Family_AFI_IP\n\t\tif ipnet.IP.To4() == nil {\n\t\t\taf = api.Family_AFI_IP6\n\t\t}\n\t\tnlri, _ := apb.New(&api.IPAddressPrefix{\n\t\t\tPrefixLen: uint32(prefixLen),\n\t\t\tPrefix:    ipnet.IP.String(),\n\t\t})\n\t\terr = bgph.server.DeletePath(ctx, &api.DeletePathRequest{\n\t\t\tTableType: api.TableType_GLOBAL,\n\t\t\tPath: &api.Path{\n\t\t\t\tNlri: nlri,\n\t\t\t\tFamily: &api.Family{\n\t\t\t\t\tAfi:  af,\n\t\t\t\t\tSafi: api.Family_SAFI_UNICAST,\n\t\t\t\t},\n\t\t\t\tPattrs: bgph.routeAttrs,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tdelCount++\n\t}\n\tif delCount == 0 {\n\t\terrs = append(errs, ErrNoRoutesDeleted)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc ValidateConfig(config *config.BGP) error {\n\tif !config.BGPEnabled {\n\t\treturn nil\n\t}\n\n\tfor _, addr := range config.AnycastAddresses {\n\t\t_, _, err := net.ParseCIDR(addr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not parse cidr for anycast address %s: %w\", addr, err)\n\t\t}\n\t}\n\n\tif config.EnableGRPC && config.GRPCTLS {\n\t\t_, err := credentials.NewServerTLSFromFile(config.CertFile, config.KeyFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not parse bgp tls credentials: %w\", err)\n\t\t}\n\t}\n\n\tfor _, peer := range config.Peers {\n\t\tif net.ParseIP(peer.NeighborAddress) == nil {\n\t\t\treturn fmt.Errorf(\"peer address %s is not a valid IP\", peer.NeighborAddress)\n\t\t}\n\t}\n\n\tif len(config.GOBGPDCfgFile) > 0 {\n\t\t_, err := bgpconfig.ReadConfigFile(config.GOBGPDCfgFile, \"toml\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not open %s: %w\", config.GOBGPDCfgFile, err)\n\t\t}\n\t\t// otherwise we skip the rest of these checks, hopefully the provided bobgpd config is sane.\n\t\treturn nil\n\t}\n\n\tif len(config.AnycastAddresses) == 0 {\n\t\treturn ErrMissingAnycast\n\t}\n\tif len(config.Peers) == 0 {\n\t\treturn ErrMissingPeers\n\t}\n\n\tif len(config.RouterID) == 0 {\n\t\treturn ErrMissingRouterID\n\t}\n\tif net.ParseIP(config.RouterID) == nil {\n\t\treturn fmt.Errorf(\"router ID %s is not a valid ID\", config.RouterID)\n\t}\n\tif len(config.NextHop) > 0 {\n\t\tif ip := net.ParseIP(config.NextHop); ip == nil {\n\t\t\treturn fmt.Errorf(\"invalid NextHop: %s\", config.NextHop)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "bgp/bgp_nonwindows_test.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage bgp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"github.com/fabiolb/fabio/config\"\n\tapi \"github.com/osrg/gobgp/v3/api\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestBGPHandler(t *testing.T) {\n\tserverCmd := &gobgpserver{\n\t\tcmdPath: \"gobgpd\",\n\t}\n\terr := serverCmd.start()\n\tif err != nil {\n\t\tt.Logf(\"error calling gobgpd command, probably not installed. skipping: %s\", err)\n\t\tt.SkipNow()\n\t}\n\tdefer serverCmd.stop()\n\tcfg := &config.BGP{\n\t\tBGPEnabled:       true,\n\t\tAsn:              65000,\n\t\tAnycastAddresses: []string{\"1.2.3.4/32\"},\n\t\tRouterID:         \"127.0.0.2\",\n\t\tListenPort:       1790,\n\t\tListenAddresses:  []string{\"127.0.0.2\"},\n\t\tPeers: []config.BGPPeer{\n\t\t\t{\n\t\t\t\tNeighborAddress: \"127.0.0.3\",\n\t\t\t\tNeighborPort:    1790,\n\t\t\t\tAsn:             65001,\n\t\t\t\tMultiHop:        false,\n\t\t\t},\n\t\t},\n\t\tEnableGRPC:        true,\n\t\tGRPCListenAddress: \"127.0.0.2:50051\",\n\t\tNextHop:           \"1.2.3.4\",\n\t}\n\tbh, err := NewBGPHandler(cfg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgo bh.server.Serve()\n\tdefer bh.server.Stop()\n\terr = bh.startBGP(context.Background())\n\tif err != nil {\n\t\tt.Fatalf(\"error starting BGP: %s\", err)\n\t}\n\terr = bh.addNeighbors(context.Background(), cfg.Peers)\n\tif err != nil {\n\t\tt.Fatalf(\"error adding neighbors: %s\", err)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*60)\n\tdefer cancel()\n\tif err := bh.server.WatchEvent(context.Background(), &api.WatchEventRequest{Peer: &api.WatchEventRequest_Peer{}}, func(r *api.WatchEventResponse) {\n\t\tif p := r.GetPeer(); p != nil && p.Type == api.WatchEventResponse_PeerEvent_STATE {\n\t\t\tt.Logf(\"EVENT RECEIVED %#v\", p.Peer)\n\t\t\tif p.Peer.State.SessionState == api.PeerState_ESTABLISHED {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t}\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t<-ctx.Done()\n\tif ctx.Err() == context.DeadlineExceeded {\n\t\tt.Fatal(\"context deadline exceeded\")\n\t}\n\n\tgc := gobpgclient{\n\t\tcmdPath:  \"gobgp\",\n\t\thostAddr: \"127.0.0.3\",\n\t}\n\n\t// now start a test table\n\n\tfor _, tst := range []struct {\n\t\tname      string\n\t\tcmd       func() error\n\t\trouteKeys []string\n\t}{\n\t\t{\n\t\t\tname: \"test add route\",\n\t\t\tcmd: func() error {\n\t\t\t\treturn bh.AddRoutes(context.Background(), cfg.AnycastAddresses)\n\t\t\t},\n\t\t\trouteKeys: []string{\"1.2.3.4/32\"},\n\t\t},\n\t\t{\n\t\t\tname: \"test delete route\",\n\t\t\tcmd: func() error {\n\t\t\t\treturn bh.DeleteRoutes(context.Background(), []string{\"1.2.3.4/32\"})\n\t\t\t},\n\t\t\trouteKeys: nil,\n\t\t},\n\t} {\n\t\tt.Run(tst.name, func(t *testing.T) {\n\t\t\terr := tst.cmd()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\troutes, err := gc.globalRib(t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif len(routes) != len(tst.routeKeys) {\n\t\t\t\tt.Fatalf(\"routes don't match, have %d want %d\",\n\t\t\t\t\tlen(routes), len(tst.routeKeys))\n\t\t\t}\n\t\t\tfor _, r := range tst.routeKeys {\n\t\t\t\tif _, ok := routes[r]; !ok {\n\t\t\t\t\tt.Fatalf(\"route %s not found\", r)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n}\n\ntype ribEntry struct {\n\tNlri struct {\n\t\tPrefix string `json:\"prefix\"`\n\t} `json:\"nlri\"`\n\tAge   int  `json:\"age\"`\n\tBest  bool `json:\"best\"`\n\tAttrs []struct {\n\t\tType    int `json:\"type\"`\n\t\tValue   int `json:\"value,omitempty\"`\n\t\tAsPaths []struct {\n\t\t\tSegmentType int   `json:\"segment_type\"`\n\t\t\tNum         int   `json:\"num\"`\n\t\t\tAsns        []int `json:\"asns\"`\n\t\t} `json:\"as_paths,omitempty\"`\n\t\tNexthop string `json:\"nexthop,omitempty\"`\n\t} `json:\"attrs\"`\n\tStale bool `json:\"stale\"`\n}\n\ntype gobpgclient struct {\n\tcmdPath  string\n\thostAddr string\n}\n\nfunc (gc *gobpgclient) globalRib(t *testing.T) (map[string][]ribEntry, error) {\n\tout, err := exec.Command(gc.cmdPath, \"-u\", gc.hostAddr, \"-j\", \"global\", \"rib\").Output()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar rv map[string][]ribEntry\n\terr = json.Unmarshal(out, &rv)\n\tif err != nil {\n\t\tt.Logf(\"raw: %s\\n\", out)\n\t\treturn nil, err\n\t}\n\treturn rv, nil\n}\n\ntype gobgpserver struct {\n\tcmdPath string\n\tcmd     *exec.Cmd\n}\n\nfunc (gs *gobgpserver) start() error {\n\tgs.cmd = exec.Command(gs.cmdPath,\n\t\t\"-p\",\n\t\t\"-f\", filepath.Join(\"test_data\", \"bgp.toml\"),\n\t\t\"--api-hosts\", \"127.0.0.3:50051\",\n\t\t\"-l\", \"info\")\n\tgs.cmd.Stdout = os.Stdout\n\tgs.cmd.Stderr = os.Stderr\n\treturn gs.cmd.Start()\n}\n\nfunc (gs *gobgpserver) stop() error {\n\tif gs.cmd.Process != nil {\n\t\treturn gs.cmd.Process.Kill()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "bgp/bgp_windows.go",
    "content": "package bgp\n\nimport (\n\t\"errors\"\n\t\"github.com/fabiolb/fabio/config\"\n)\n\ntype BGPHandler struct{}\n\nvar ErrNoWindows = errors.New(\"cannot run bgp on windows\")\n\nfunc NewBGPHandler(config *config.BGP) (*BGPHandler, error) {\n\treturn nil, ErrNoWindows\n}\n\nfunc (bgph *BGPHandler) Start() error {\n\treturn ErrNoWindows\n}\n\nfunc ValidateConfig(config *config.BGP) error {\n\treturn ErrNoWindows\n}\n"
  },
  {
    "path": "bgp/logger.go",
    "content": "package bgp\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/exit\"\n\t\"github.com/fabiolb/fabio/logger\"\n\n\tbgplog \"github.com/osrg/gobgp/v3/pkg/log\"\n)\n\ntype bgpLogger struct{}\n\nfunc (l bgpLogger) Panic(msg string, fields bgplog.Fields) {\n\texit.Fatal(convertMsgFields(\"FATAL\", msg, fields))\n}\n\nfunc (l bgpLogger) Fatal(msg string, fields bgplog.Fields) {\n\texit.Fatal(convertMsgFields(\"FATAL\", msg, fields))\n}\n\nfunc (l bgpLogger) Error(msg string, fields bgplog.Fields) {\n\tlog.Printf(\"%s\", convertMsgFields(\"ERROR\", msg, fields))\n}\n\nfunc (l bgpLogger) Warn(msg string, fields bgplog.Fields) {\n\tlog.Printf(\"%s\", convertMsgFields(\"WARN\", msg, fields))\n}\n\nfunc (l bgpLogger) Info(msg string, fields bgplog.Fields) {\n\tlog.Printf(\"%s\", convertMsgFields(\"INFO\", msg, fields))\n}\n\nfunc (l bgpLogger) Debug(msg string, fields bgplog.Fields) {\n\tlog.Printf(\"%s\", convertMsgFields(\"DEBUG\", msg, fields))\n}\n\nfunc (l bgpLogger) SetLevel(level bgplog.LogLevel) {\n\t// noop\n}\n\nfunc (l bgpLogger) GetLevel() bgplog.LogLevel {\n\tlw, ok := log.Writer().(*logger.LevelWriter)\n\tif !ok {\n\t\treturn bgplog.InfoLevel\n\t}\n\n\tswitch lw.Level() {\n\tcase \"TRACE\":\n\t\treturn bgplog.TraceLevel\n\tcase \"DEBUG\":\n\t\treturn bgplog.DebugLevel\n\tcase \"INFO\":\n\t\treturn bgplog.InfoLevel\n\tcase \"WARN\":\n\t\treturn bgplog.WarnLevel\n\tcase \"ERROR\":\n\t\treturn bgplog.ErrorLevel\n\tcase \"FATAL\":\n\t\treturn bgplog.FatalLevel\n\tdefault:\n\t\treturn bgplog.InfoLevel\n\t}\n}\n\nfunc convertMsgFields(level, msg string, fields bgplog.Fields) string {\n\tvar b strings.Builder\n\tfmt.Fprintf(&b, \"[%s] gobgpd %s\", level, msg)\n\tfor k, v := range fields {\n\t\tfmt.Fprintf(&b, \" %s=>%v\", k, v)\n\t}\n\treturn b.String()\n}\n"
  },
  {
    "path": "bgp/test_data/bgp.toml",
    "content": "[[neighbors]]\n  [neighbors.config]\n    neighbor-address = \"127.0.0.2\"\n    peer-as = 65000\n  [neighbors.transport.config]\n    remote-port = 1790\n    passive-mode = false\n    local-address = \"127.0.0.3\"\n\n[global.config]\n  as = 65001\n  router-id = \"127.0.0.3\"\n  port = 1790\n  #port = 179\n  local-address-list = [ \"127.0.0.3\" ]\n"
  },
  {
    "path": "build/ca-certificates.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE\nAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x\nCzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW\nMBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF\nRElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC\nAgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7\n09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7\nXBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P\nGrjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK\nt0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb\nX79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28\nMHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU\nfecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI\n2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH\nK9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae\nZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP\nBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ\nMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw\nRAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv\nbS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm\nfQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3\ngvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe\nI6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i\n5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi\nipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn\nMCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ\no5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6\nzqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN\nGHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt\nr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK\nZ05phkOTOPu220+DkdRgfks+KzgHVZhepA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsx\nCzAJBgNVBAYTAkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRp\nZmljYWNpw7NuIERpZ2l0YWwgLSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwa\nQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4wHhcNMDYxMTI3MjA0NjI5WhcNMzAw\nNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2Ft\nZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMu\nQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeG\nqentLhM0R7LQcNzJPNCNyu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzL\nfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQ\nY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU34ojC2I+GdV75LaeHM/J4\nNy+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP2yYe68yQ\n54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+b\nMMCm8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48j\nilSH5L887uvDdUhfHjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++Ej\nYfDIJss2yKHzMI+ko6Kh3VOz3vCaMh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/zt\nA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK5lw1omdMEWux+IBkAC1vImHF\nrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1bczwmPS9KvqfJ\npxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE\nAwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCB\nlTCBkgYEVR0gADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFy\nYS5jb20vZHBjLzBaBggrBgEFBQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW50\n7WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2UgcHVlZGVuIGVuY29udHJhciBlbiBs\nYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEfAygPU3zmpFmps4p6\nxbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuXEpBc\nunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/\nJre7Ir5v/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dp\nezy4ydV/NgIlqmjCMRW3MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42\ngzmRkBDI8ck1fj+404HGIGQatlDCIaR43NAvO2STdPCWkPHv+wlaNECW8DYSwaN0\njJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wkeZBWN7PGKX6jD/EpOe9+\nXCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f/RWmnkJD\nW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/\nRL5hRqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35r\nMDOhYil/SrnhLecUIw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxk\nBYn8eNZcLCZDqQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU\nMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs\nIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290\nMB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux\nFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h\nbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v\ndDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt\nH7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9\nuMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX\nmk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX\na0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN\nE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0\nWicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD\nVR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0\nJvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU\ncnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx\nIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN\nAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH\nYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5\n6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC\nNr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX\nc4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a\nmnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU\nMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3\nb3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw\nMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML\nQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD\nVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul\nCDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n\ntGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl\ndI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch\nPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC\n+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O\nBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl\nMQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk\nZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB\nIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X\n7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz\n43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY\neDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl\npz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA\nWiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU\nMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3\nb3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx\nMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB\nZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV\nBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV\n6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX\nGCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP\ndzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH\n1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF\n62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW\nBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw\nAwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL\nMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU\ncnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv\nb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6\nIBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/\niHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao\nGEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh\n4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm\nXiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU\nMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3\nb3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1\nMzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK\nEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh\nBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq\nxBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G\n87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i\n2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U\nWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1\n0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G\nA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr\npGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL\nExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm\naWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv\nhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm\nhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X\ndgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3\nP6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y\niQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no\nxqE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP\nHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr\nba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL\nMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\nyHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr\nVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/\nnx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG\nXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj\nvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt\nZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g\nN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC\nnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz\ndCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL\nMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp\ncm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y\nYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua\nkCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL\nQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\n6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG\nyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i\nQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ\nKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO\ntDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu\nQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ\nLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u\nolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48\nx3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE\nBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz\ndCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG\nA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U\ncnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf\nqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ\nJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ\n+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS\ns8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\nHMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7\n70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG\nV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S\nqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S\n5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia\nC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX\nOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE\nFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\nKI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg\nNt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B\n8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ\nMKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc\n0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ\nu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF\nu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH\nYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8\nGKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\nRtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e\nKeC2uAloGRwYQw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC\nVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ\ncmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ\nBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt\nVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D\n0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9\nss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G\nA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\naobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I\nflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP\nbmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2\nMDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft\nZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg\nQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk\nhsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym\n1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW\nOqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb\n2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko\nO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU\nAK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB\nBQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF\nZu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb\nLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir\noQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C\nMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds\nsPmuujz9dLQR6FgNgLzTqIA6me11zEZ7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP\nbmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2\nMDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft\nZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg\nQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC\n206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci\nKtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2\nJxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9\nBoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e\nXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B\nPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67\nXnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq\nZ8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ\no2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3\n+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj\nYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj\nFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE\nAwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn\nxPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2\nLHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc\nobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8\nCNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe\nIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA\nDjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F\nAjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX\nOm/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb\nAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl\nZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw\nRY8mkaKO/qk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc\nMBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp\nb25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT\nAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs\naWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H\nj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K\nf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55\nIrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw\nFO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht\nQWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm\n/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ\nk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ\nMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC\nseODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ\nhyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+\neKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U\nDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj\nB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL\nrosot4LKGAfmt1t06SAZf7IbiVQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB\nVDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp\nbSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R\ndWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw\nMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy\ndXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52\nZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM\nEEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj\nlUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ\nznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH\n2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1\nk3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs\n2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD\nVR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC\nAQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG\nKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+\n8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R\nFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS\nmYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE\nDNuxUCAKGkq6ahq97BvIxYSazQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE\nBhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h\ncHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy\nMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg\nQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9\nthDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM\ncas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG\nL9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i\nNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h\nX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b\nm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy\nZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja\nEbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T\nKI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF\n6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh\nOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD\nVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD\nVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp\ncm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv\nACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl\nAGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF\n661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9\nam58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1\nILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481\nPyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS\n3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k\nSeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF\n3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM\nZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g\nStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz\nQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB\njLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\nRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\nVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\nDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\nZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\nVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\nmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\nIZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\nmpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\nXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\ndc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\njl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\nBE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\njkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\nEpn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\nksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\nR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg\nQ2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL\nMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD\nVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0\nojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX\nl18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB\nHfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B\n5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3\nWNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD\nAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP\ngcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+\nDKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu\nBctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs\nh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk\nLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg\nQ2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL\nMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD\nVQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg\nisRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z\nNIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI\n+MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R\nhzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+\nmbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD\nAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP\nBdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s\nEzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2\nmSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC\ne/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow\ndXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET\nMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE\nAxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw\nCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg\nYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE\nNx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX\nmjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD\nXcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW\nS8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp\nFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw\nAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD\nAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu\nZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z\nay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv\nY2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw\nDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6\nyKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq\nEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/\nCBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB\nEicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN\nPGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn\nMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL\nExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg\nb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa\nMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB\nODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw\nIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B\nAQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb\nunXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d\nBmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq\n7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3\n0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX\nroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG\nA1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j\naGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p\n26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA\nBzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud\nEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN\nBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz\naWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB\nAAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd\np0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi\n1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc\nXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0\neDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu\ntGWaIZDgqtCYvDi1czyL+Nw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn\nMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL\nExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo\nYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9\nMQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy\nNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G\nA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA\nA4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0\nMi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s\nQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV\neAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795\nB9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh\nz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T\nAQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i\nZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w\nTcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH\nMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD\nVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE\nVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh\nbWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B\nAQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM\nbKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi\nryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG\nVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c\necQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/\nAYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV\nBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X\nDTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ\nBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4\nQCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny\ngQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw\nzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q\n130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\nJsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw\nZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT\nAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj\nAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG\n9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h\nbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc\nfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu\nHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\nt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw\nWyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET\nMBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk\nBgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4\nMjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl\ncnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0\naW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY\nF1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N\n8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe\nrP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K\n/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu\n7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC\n28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6\nlSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E\nnn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB\n0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09\n5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj\nWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN\njLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ\nKoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s\nov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM\nOH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q\n619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn\n2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj\no3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v\nnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG\n5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq\npdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb\ndsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0\nBLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw\nPTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz\ncyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9\nMQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz\nIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ\nltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR\nVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL\nkcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd\nEgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas\nH7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0\nHGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud\nDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4\nQgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu\nY29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/\nAN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8\nyfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR\nFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA\nybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB\nkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7\nl7+ijrRU\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT\nAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD\nQTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP\nMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do\n0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ\nUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d\nRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ\nOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\nJoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C\nAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O\nBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ\nLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY\nMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ\n44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I\nJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw\ni/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN\n9u6wWk5JRFRYX0KD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM\nMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD\nQTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM\nMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD\nQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E\njG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo\nePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI\nULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu\nOb7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg\nAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7\nHVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA\nuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa\nTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg\nxSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q\nCjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x\nO/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs\n6GAqm4VKQPNriiTsBhYscw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM\nMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D\nZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU\ncnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3\nWjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg\nUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw\nIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH\nUV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM\nTXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU\nBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM\nkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x\nAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV\nHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y\nsHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL\nI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8\nJ9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY\nVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI\n03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD\nVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0\nIHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3\nMRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz\nIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz\nMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj\ndXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw\nEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp\nMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G\nCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9\n28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq\nVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q\nDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR\n5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL\nZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a\nSd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl\nUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s\n+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5\nWk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj\nya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx\nhduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV\nHQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1\n+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN\nYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t\nL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy\nZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt\nIDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV\nHSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w\nDQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW\nPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF\n5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1\nglanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH\nFoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2\npSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD\nxvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG\ntjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq\njktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De\nfhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg\nOGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ\nd0jQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD\nTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2\nMDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF\nQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh\nIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6\ndLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO\nV/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC\nGHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN\nv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB\nAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB\nAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO\n76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK\nOOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH\nugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi\nyJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL\nbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj\n2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\nYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\nGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\nBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\n3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\nYgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\nrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\nez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\noBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\nMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\nQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\nb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\nAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\nGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\nRt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\nG9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\nl2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\nsmPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB\ngTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV\nBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw\nMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl\nYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P\nRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0\naG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3\nUcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8\nQ5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp\n+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+\nDT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O\nnKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW\n/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g\nPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u\nQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY\nSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\nIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/\nRxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4\nzJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd\nBA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB\nZQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL\nMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\nBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT\nIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw\nMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy\nZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N\nT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv\nbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR\nFtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J\ncfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW\nBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/\nBAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm\nfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv\nGDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp\nZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow\nfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\nA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV\nBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM\ncm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S\nHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996\nCF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk\n3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz\n6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV\nHQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud\nEwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv\nY2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw\nOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww\nDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0\n5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj\nZ55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI\ngKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ\naD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl\nizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb\nMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\nGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0\naWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla\nMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\nBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD\nVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW\nfnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt\nTGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL\nfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW\n1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7\nkUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G\nA1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD\nVR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v\nZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo\ndHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu\nY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/\nHrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32\npSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS\njBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+\nxqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn\ndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0\nMRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG\nEwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT\nCkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK\n8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2\n98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb\n2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC\nejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi\nXd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB\no4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl\nZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD\nAgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL\nAZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd\nfoPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M\ncXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq\n8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp\nhbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk\nRes3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U\nAGegcQCCSA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw\nPDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu\nMQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx\nGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL\nMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf\nHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh\ngHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW\nv+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue\nMv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr\n9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt\n6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7\nMDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl\nY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58\nADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq\nhkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p\niL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC\ndsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL\nkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL\nhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz\nOjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG\nA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh\nbCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE\nChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS\nb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5\n7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS\nJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y\nHLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP\nt3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz\nFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY\nXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/\nMB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw\nhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js\nMB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA\nA4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj\nWqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx\nXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o\nomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc\nA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW\nWL1WMRJOEcgh4LMRkWXbtKaIOM5V\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc\nMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj\nIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB\nIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE\nRTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl\nU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290\nIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU\nha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC\nQN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr\nrFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S\nNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc\nQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH\ntxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP\nBgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC\nAQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp\ntJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa\nIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl\n6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+\nxbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU\nCm26OWMohpLzGITY+9HPBVZkVw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\nJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\nmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\nwRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\nVYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\nAUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\nAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\npyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\ndWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\nfwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\nNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\nH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\n+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\nQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\nCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\nnh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\nT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\ngdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\nBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\nTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\nDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\nhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\nPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\nYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\nCAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\nZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\nLmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\nRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\nPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\nxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\nIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\nhzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\nEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\nFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\nnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\neM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\nhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\nYzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\nvEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n+OkuE6N36B9K\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV\nUzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL\nEwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ\nBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x\nETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg\nbIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ\nj9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV\nSn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG\nSAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx\nJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI\nRFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw\nMjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5\nfpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i\n+DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG\nSIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN\nQseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+\ngG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV\nUzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL\nEwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ\nBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x\nETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/\nk48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso\nLeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o\nTQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG\nSAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx\nJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI\nRFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3\nMjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C\nTShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5\nWzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG\nSIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR\nxdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL\nB3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx\nETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w\nMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD\nVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx\nFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu\nktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7\ngLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH\nfAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a\nahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT\najV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF\nMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk\nc3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto\ndHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt\naW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI\nhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk\nQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/\nh40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq\nnExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR\nrscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2\n9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV\nBAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx\nc8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt\nZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4\nMTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg\nSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl\na25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h\n4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk\ntiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s\ntPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL\ndlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4\nc0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um\nTDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z\n+kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O\nLna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW\nOeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW\nfo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2\nl9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB\n/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw\nFoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+\n8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI\n6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO\nTLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME\nwfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY\nIai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn\nxk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q\nDgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q\nKd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t\nhie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4\n7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7\nQPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxp\nZ2kgQS5TLjE8MDoGA1UEAxMzZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZp\na2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3MDEwNDExMzI0OFoXDTE3MDEwNDEx\nMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0cm9uaWsgQmlsZ2kg\nR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9uaWsg\nU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdU\nMZTe1RK6UxYC6lhj71vY8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlT\nL/jDj/6z/P2douNffb7tC+Bg62nsM+3YjfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H\n5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAIJjjcJRFHLfO6IxClv7wC\n90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk9Ok0oSy1\nc+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoE\nVtstxNulMA0GCSqGSIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLP\nqk/CaOv/gKlR6D1id4k9CnU58W5dF4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S\n/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwqD2fK/A+JYZ1lpTzlvBNbCNvj\n/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4Vwpm+Vganf2X\nKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq\nfJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML\nRW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp\nbmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5\nIEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3\nMjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3\nLmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp\nYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG\nA1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq\nK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe\nsYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX\nMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT\nXTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/\nHoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH\n4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub\nj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo\nU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf\nzX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b\nu/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+\nbYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er\nfF6adulZkMV8gzURZVE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\nVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0\nLm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW\nKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl\ncnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw\nNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw\nNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy\nZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV\nBAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo\nNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4\n4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9\nKlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI\nrb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi\n94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB\nsDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi\ngA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo\nkORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE\nvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA\nA4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t\nO1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua\nAGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP\n9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/\neu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m\n0vdXcDazv/wor3ElhVsT/h5/WrQ8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe\nMQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0\nZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\nFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw\nIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL\nSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH\nSyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh\nijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X\nDZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1\nTBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ\nfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA\nsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU\nWH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS\nnT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH\ndmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip\nNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC\nAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF\nMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH\nClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB\nuvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl\nPwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP\nJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/\ngpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2\nj6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6\n5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB\no2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS\n/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z\nGp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE\nW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D\nhNQ+IIX3Sj0rnP0qCglN6oH4EZw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV\nUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy\ndGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1\nMVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx\ndWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B\nAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f\nBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A\ncJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC\nAwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ\nMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm\naWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw\nODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj\nIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF\nMAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA\nA4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y\n7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh\n1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT\nZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw\nMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j\nLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ\nKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo\nRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu\nWqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw\nEnv+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD\nAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK\neDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM\nzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+\nWB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN\n/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc\nMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT\nZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw\nMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj\ndXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l\nc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC\nUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc\n58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/\no5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH\nMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr\naGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA\nA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA\nZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv\n8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs\nIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG\nEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg\nR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A\nPRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8\nY2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL\nTytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL\n5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7\nS4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe\n2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE\nFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap\nEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td\nEPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv\n/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN\nA0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0\nabby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF\nI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz\n4iIprn2DQKi6bA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\nYWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG\nEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg\nR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9\n9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq\nfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv\niS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU\n1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+\nbw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW\nMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA\nephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l\nuMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn\nZ57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS\ntQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF\nPseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un\nhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV\n5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY\nMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo\nR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx\nMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK\nEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp\nZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9\nAWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA\nZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0\n7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W\nkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI\nmO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G\nA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ\nKoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1\n6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl\n4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K\noKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj\nUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU\nAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL\nMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj\nKSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2\nMDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\neSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV\nBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw\nNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV\nBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH\nMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL\nSo17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal\ntJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG\nCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT\nqQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz\nrD6ogRLQy7rQkgu2npaqBA+K\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB\nmDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT\nMChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s\neTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv\ncml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ\nBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg\nMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0\nBgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz\n+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm\nhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn\n5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W\nJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL\nDmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC\nhuOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw\nHQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB\nAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB\nzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN\nkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD\nAWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH\nSJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G\nspki4cErx5z481+oghLrGREt\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy\nc2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD\nVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1\nc3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC\nAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81\nWzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG\nFF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq\nXbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL\nse4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb\nKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd\nIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73\ny/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt\nhAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc\nQIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4\nLt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV\nHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ\nKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z\ndXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ\nL1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr\nFg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo\nag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY\nT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz\nGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m\n1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV\nOCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH\n6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX\nQMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW\nMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy\nc2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE\nBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0\nIFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV\nVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8\ncQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT\nQjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh\nF7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v\nc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w\nmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd\nVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX\nteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ\nf9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe\nBi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+\nnhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB\n/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY\nMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG\n9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc\naanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX\nIwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn\nANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z\nuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN\nPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja\nQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW\nkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9\nER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt\nDF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm\nbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD\nVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0\nIHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3\nMRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD\naGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx\nMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy\ncmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG\nA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl\nBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI\nhvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed\nKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7\nG706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2\nzxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4\nddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG\nHoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2\nId3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V\nyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e\nbeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r\n6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh\nwZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog\nzCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW\nBBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr\nru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp\nZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk\ncmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt\nYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC\nCQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow\nKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI\nhvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ\nUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz\nX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x\nfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz\na2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd\nYhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd\nSqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O\nAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso\nM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge\nv8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z\n09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\nA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\nb3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\nMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\nYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\naWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\njc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\nxy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\nsnUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\nU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\nAQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\nyj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\nAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\nDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\nHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1\nMDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL\nv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8\neoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq\ntTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd\nC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa\nzq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB\nmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH\nV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n\nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG\n3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs\nJ0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO\n291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS\not+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd\nAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7\nTBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\nA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\nZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\nMTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\nA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\nRgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\ngHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\nKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\nQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\nXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\nDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\nLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\nRUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\njjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\nmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\nMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\nWD9f\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh\nMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE\nYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3\nMDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo\nZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg\nMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN\nADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA\nPVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w\nwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi\nEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY\navx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+\nYihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE\nsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h\n/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5\nIEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD\nggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy\nOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P\nTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ\nHmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER\ndEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf\nReYNnyicsbkqWletNw+vHX/bvZ8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT\nEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp\nZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz\nNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH\nEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE\nAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD\nE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH\n/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy\nDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh\nGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR\ntDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA\nAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX\nWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu\n9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr\ngIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo\n2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO\nLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI\n4uJEvlz36hz1\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD\nVQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv\nbHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv\nb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV\nUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU\ncnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds\nb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH\niM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS\nr41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4\n04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r\nGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9\n3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P\nlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx\nFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg\nUm9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG\nA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr\nb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ\njVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn\nPzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh\nZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9\nnnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h\nq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED\nMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC\nmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3\n7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB\noiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs\nEhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO\nfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi\nAmvZWg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT\nAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ\nTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG\n9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw\nMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM\nBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO\nMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2\nLmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI\ns9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2\nxtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4\nu0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b\nF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx\nVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd\nPDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV\nHSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx\nNjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF\nAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ\nL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY\nYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg\nCrpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a\nNjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R\n0982gaEbeC9xs/FZTEYYKKuF0mBWWg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4\nMQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6\nZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD\nVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j\nb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq\nscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO\nxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H\nLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX\nuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\nyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+\nJrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q\nrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN\nBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L\nhij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB\nQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+\nHMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu\nZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg\nQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\nBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx\nMCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA\nA4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb\nlaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56\nawmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo\nJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw\nLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT\nVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\nLhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb\nUjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/\nQnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+\nnaM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls\nQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN\nAQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp\ndHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw\nMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw\nCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ\nMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB\nSvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz\nABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH\nLCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP\nPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL\n2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w\nggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC\nMIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk\nAGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0\nAHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz\nAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz\nAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f\nBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE\nFASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY\nP2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi\nCfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g\nkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95\nHvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS\nna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q\nqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z\nTbvGRNs2yyqcjg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD\nVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0\nZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G\nCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y\nOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx\nFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp\nZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o\ndTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP\nkd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc\ncbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U\nfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7\nN4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC\nxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1\n+rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM\nPcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG\nSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h\nmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk\nddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775\ntyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c\n2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t\nHMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw\ncjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy\nb3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z\nZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4\nNDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN\nTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p\nY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u\nuO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+\nLMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA\nvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770\nYjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx\n62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB\nAQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw\nLQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP\nBgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB\nAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov\nMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5\nACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn\nAGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT\nAHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh\nACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo\nAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa\nAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln\nbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p\nY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP\nPU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv\nY2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB\nEGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu\nw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj\ncm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV\nHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI\nVTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS\nBgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS\nb290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS\n8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds\nZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl\n7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a\n86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR\nhUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/\nMPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG\nEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3\nMDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl\ncnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR\ndGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB\npzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM\nb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm\naWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz\nIEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT\nlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz\nAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5\nVA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG\nILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2\nBJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG\nAQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M\nU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh\nbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C\n+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC\nbLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F\nuLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2\nXjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx\nETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0\nb25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD\nEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05\nOTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G\nA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh\nZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l\ndExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG\nSIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK\ngZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX\niK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc\nQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E\nBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G\nSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu\nb3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh\nbGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv\nY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln\naXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0\nIGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh\nc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph\nbiBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo\nZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP\nUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj\nYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo\ndHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA\nbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06\nsPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa\nn3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS\nNitjrFgBazMpUIaD8QFI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx\nETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0\nb25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD\nEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X\nDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw\nDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u\nc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr\nTmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN\nBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA\nOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC\n2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW\nRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P\nAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW\nggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0\nYWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz\nb2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO\nZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB\nIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs\nb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs\nZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s\nYXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg\na2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g\nSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0\naWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg\nYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg\nY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY\nta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g\npO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4\nFp1hBWeAyNDYpQcCNJgEjTME1A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV\nMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe\nTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0\ndmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB\nKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0\nN1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC\ndWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu\nMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL\nb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD\nzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi\n3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8\nWgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY\nOph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi\nNCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC\nApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4\nQgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0\nYW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz\naSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu\nIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm\nZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg\nZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs\namFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv\nIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3\nLm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6\nZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1\nYW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg\ndG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs\nb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G\nCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO\nxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP\n0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ\nQeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk\nf1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK\n8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx\nETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0\nb25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD\nEzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz\naXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w\nMzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G\nA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh\nZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l\ndExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh\nbnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq\nhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq\neKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe\nr7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5\n3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd\nvLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l\nmT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC\nwDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg\nhkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0\nTG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh\nbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg\nZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg\ndmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6\nb2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl\nc2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0\nZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3\ndy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu\nZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh\nbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo\nZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3\nLm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u\nZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA\nA4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ\nMznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+\nNFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR\nVCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY\n83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3\nmacqaJVmlaut74nLYKkGEsaUR+ko\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi\nMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu\nMTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp\ndHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV\nUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO\nZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz\nc7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP\nOCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl\nmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF\nBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4\nqY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw\ngZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB\nBjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu\nbmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp\ndHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8\n6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/\nh1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH\n/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv\nwKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN\npGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB\nijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly\naWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl\nZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w\nNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G\nA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD\nVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX\nSVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR\nVVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2\nw93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF\nmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg\n4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9\n4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw\nDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw\nEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx\nSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2\nftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8\nvPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa\nhNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi\nFj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ\n/L7fCg0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa\nGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg\nFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J\nWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB\nrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1\nksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i\nUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz\nPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og\n/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH\noycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI\nyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud\nEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2\nA8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\nMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT\nElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f\nBluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn\ng/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl\nfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K\nWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha\nB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc\nhLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR\nTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\nmbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z\nohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y\n4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza\n8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x\nGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv\nb3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV\nBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W\nYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM\nV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB\n4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr\nH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd\n8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\nvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT\nmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe\nbtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc\nT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt\nWAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ\nc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A\n4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD\nVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG\nCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\naXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0\naWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu\ndC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw\nczALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G\nA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC\nTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg\nUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0\n7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem\nd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\n+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B\n4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN\nt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x\nDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57\nk8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s\nzHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j\nWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT\nmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK\n4SVhM7JZG+Ju1zdXtg2pEto=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC\nTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz\nMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw\nIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR\ndW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp\nli4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D\nrOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ\nWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug\nF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU\nxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC\nAk4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv\ndmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw\nggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl\nIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh\nc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy\nZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh\nY3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI\nKwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T\nKbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq\ny+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p\ndGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD\nVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL\nMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk\nfnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8\n7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R\ncHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y\nmQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW\nxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK\nSnQ2+Q==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF\nUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ\nR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN\nMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G\nA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw\nJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+\nWmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj\nSgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl\nu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy\nA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk\nHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7\nMIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr\naS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC\nIwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A\ncgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA\nYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA\nbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA\nbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA\naQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA\naQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA\nZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA\nYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA\nZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA\nLgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6\nLy93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y\neAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw\nCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G\nA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu\nY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn\nlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt\nb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg\n9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF\nducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC\nIoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6\nMRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp\ndHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX\nBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy\nMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp\neafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg\n/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl\nwSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh\nAMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2\nPcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu\nAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR\nMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc\nHnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/\nZb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+\nf00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO\nrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch\n6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3\n7CAFYd4=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\nMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\nQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\niQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\n/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\njnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\nHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\nsFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\ngZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\nMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\nKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\nAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\nURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\nH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\nI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\niNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\nf8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr\nMCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG\nA1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0\nMDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp\nY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD\nQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz\ni1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8\nh9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV\nMdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9\nUK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni\n8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC\nh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD\nVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\nAKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm\nKbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ\nX5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr\nQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5\npPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN\nQSdJQO7e5iNEOdyhIta6A/I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\nMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\nFzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\nMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\ncnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\nZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\n0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\nwW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\n8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\nBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\nJYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\nNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\n6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\n3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\nD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\nCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\n3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz\nMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N\nIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11\nbmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE\nRMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO\nzXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5\nbmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF\nMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1\nVkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC\nOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G\nCSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW\ntWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ\nq51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb\nEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+\nQi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O\nVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY\nMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t\ndW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5\nWjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD\nVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8\n9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ\nDKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9\nMs+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N\nQV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ\nxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G\nA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG\nkl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr\nUj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5\nBw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU\nJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot\nRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP\nMA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx\nMDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV\nBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG\n29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk\noVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk\n3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL\nqdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN\nnvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw\nDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG\nMA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX\nZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H\nDjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO\nTzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv\nkVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w\nzMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP\nMA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx\nMDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV\nBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o\nZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt\n5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s\n3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej\nvOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu\n8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw\nDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG\nMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil\nzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/\n3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD\nFNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6\nTk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2\nZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO\nTDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh\ndCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy\nMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk\nZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn\nExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71\n9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO\nhXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U\ntFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o\nBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh\nSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww\nOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv\ncm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA\n7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k\n/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm\neafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6\nu3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy\n7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR\niJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO\nTDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh\ndCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX\nDTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl\nciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv\nb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291\nqj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp\nuOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU\nZ5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE\npMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp\n5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M\nUGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN\nGmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy\n5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv\n6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK\neN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6\nB6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/\nBAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov\nL3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG\nSIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS\nCZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen\n5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897\nIZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK\ngnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL\n+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL\nvJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm\nbEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk\nN1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC\nY7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z\nywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl\nMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp\nU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw\nNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE\nChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp\nZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3\nDQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf\n8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN\n+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0\nX9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa\nK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA\n1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G\nA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR\nzt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0\nYXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD\nbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w\nDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3\nL7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D\neruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl\nxy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp\nVSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY\nWQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs\nZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw\nMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6\nb25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj\naG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp\nY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg\nnLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1\nHOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N\nHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN\ndloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0\nHZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G\nCSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU\nsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3\n4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg\n8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K\npL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1\nmMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\nEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\nHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\nZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\nMDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\nVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\nZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\ndmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\nOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\n8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\nTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\nhRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\n6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\nDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\nAdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\nbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\nve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\nqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\niEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\n0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\nsSi6\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg\nQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9\nMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi\nU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh\ncnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk\npMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf\nOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C\nJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT\nKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi\nHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM\nAv+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w\n+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+\nGkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3\nZzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B\n26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID\nAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE\nFE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j\nZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js\nLnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM\nBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0\nY29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy\ndGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh\ncnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh\nYmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg\ndGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp\nbGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ\nYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT\nTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ\n9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8\njhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW\nFjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz\newT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1\nny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L\nEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu\nL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq\nyvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC\nO3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V\num0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh\nNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB\nrjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp\nMRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz\nc2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u\nIGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa\nFw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t\nV3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg\nRGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV\nU1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1\ntoPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo\nTUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy\nggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1\nXgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF\nhy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm\n7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG\nMCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV\nHQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp\nttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD\npwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo\nLtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF\niXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y\nh9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I\nk63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk\nMQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0\nYWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg\nQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT\nAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp\nY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9\nm2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih\nFvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/\nTilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F\nEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco\nkdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu\nHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF\nvJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo\n19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC\nL3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW\nbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX\nJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw\nFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j\nBBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc\nK6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf\nky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik\nVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB\nsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e\n3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR\nls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip\nmXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH\nb6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf\nrK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms\nhFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y\nzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6\nMBr1mmz0DlP5OlvRHA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\nBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln\nbiBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF\nMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT\nd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8\n76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+\nbbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c\n6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\nemA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd\nMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt\nMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y\nMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y\nFGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi\naG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM\ngI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB\nqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7\nlqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\n8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov\nL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6\n45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO\nUYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5\nO1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC\nbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv\nGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a\n77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC\nhdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\n92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp\nLd6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w\nZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt\nQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu\nIFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw\nWjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD\nExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y\nIIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn\nIuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+\n6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob\njM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw\nizSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl\n+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY\nzt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP\npZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF\nKwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW\nae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB\nAAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0\nZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW\nIGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA\nA4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0\nuMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+\nFHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7\njposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/\nu0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D\nYSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1\npuEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa\nicYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG\nDI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x\nkcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z\nWr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE\nBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu\nIFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow\nRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY\nU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\nMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv\nFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br\nYT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF\nnbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH\n6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt\neJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/\nc8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ\nMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH\nHTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf\njNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6\n5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB\nrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c\nwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0\ncDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB\nAHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp\nWJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9\nxCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ\n2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ\nIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8\naRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X\nem1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR\ndAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/\nOMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+\nhAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy\ntGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/\nMQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj\nYXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow\nPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp\nY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB\nAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR\nIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q\ngQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy\nyhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts\nF/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2\njWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx\nls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC\nVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK\nYS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH\nEgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN\nXo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud\nDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE\nMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK\nUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ\nTulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf\nqzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK\nZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE\nJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7\nhUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1\nEqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm\nnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX\nudpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz\nssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe\nLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl\npYYsfPQS\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV\nBAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0\nQ2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1\nOTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i\nSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc\nVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf\ntMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg\nuNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J\nXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK\n8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99\n5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3\nkUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy\ndXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6\nLy93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz\nJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290\nY2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u\nTGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS\nGNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt\nZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8\nau0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV\nhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI\ndUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV\nBAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0\nQ2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1\nOTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i\nSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc\nVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW\nHt4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q\nVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2\n1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq\nukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1\nRb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX\nXAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy\ndXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6\nLy93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz\nJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290\nY2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u\nTGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN\nirTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8\nTtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6\ng0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB\n95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj\nS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL\nMAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV\nBAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1\nc3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx\nMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg\nR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD\nVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR\nJJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T\nfCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu\njRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z\nwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ\nfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD\nVR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO\nBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G\nCSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1\n7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn\n8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs\nydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT\nujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/\n2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD\nVQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv\nbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy\ndmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t\nMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB\nMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG\nA1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp\nb24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl\ncnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv\nbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE\nVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ\nug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR\nuHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG\n9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI\nhfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM\npAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB\nqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf\nQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw\nMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV\nBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw\nNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j\nLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG\nA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl\nIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs\nW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta\n3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk\n6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6\nSk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J\nNqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA\nMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP\nr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU\nDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz\nYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX\nxPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2\n/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/\nLHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7\njVaMaA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL\nMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp\nIDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi\nBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw\nMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh\nd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig\nYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v\ndCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/\nBebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6\npapu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K\nDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3\nKMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox\nXZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB\nrjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf\nQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw\nMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV\nBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa\nFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl\nLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u\nMTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl\nZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm\ngcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8\nYZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf\nb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9\n9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S\nzhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk\nOQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV\nHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA\n2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW\noCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu\nt8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c\nKUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM\nm7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu\nMdRAGmI0Nj81Aa6sY6A=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx\nFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD\nVQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv\nbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm\nMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx\nMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT\nDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3\ndGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl\ncyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3\nDQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD\ngY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91\nyekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX\nL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj\nEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG\n7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e\nQNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ\nqdq5snUb9kLy78fyGPmJvKP/iiMucEc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS\nMRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp\nbGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw\nVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy\nYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy\ndGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2\nayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe\nFw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx\nGDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls\naW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU\nQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh\nxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0\naWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr\nIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h\ngb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK\nO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO\nfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw\nlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL\nhmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID\nAQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP\nNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t\nwyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM\n7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh\ngLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n\noN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs\nyZyQ2uypQjyttgI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc\nUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx\nc8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg\nMjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8\ndmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz\nMjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy\ndGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD\nVQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg\nxLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu\nxZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7\nXfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k\nheiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J\nYbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C\nurKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1\nJuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51\nb0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV\n9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7\nkjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh\nfEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy\nB0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA\naLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS\nRGQDJereW26fyfJOrN3H\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc\nUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx\nc8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS\nS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg\nSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3\nWhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv\nbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU\nUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw\nbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe\nLiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef\nJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh\nR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ\nQv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX\nJHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p\nzpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S\nFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ\nKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq\nECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4\nJl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz\ngw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH\nuFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS\ny3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES\nMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU\nV0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz\nWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO\nLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE\nAcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH\nK3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX\nRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z\nrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx\n3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq\nhkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC\nMErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls\nXebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D\nlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn\naspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ\nYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB\nkzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug\nQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho\ndHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw\nIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG\nEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD\nVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu\ndXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6\nE5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ\nD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK\n4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq\nlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW\nbfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB\no4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT\nMtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js\nLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr\nBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB\nAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft\nGzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj\nj98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH\nKWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv\n2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3\nmfnGV/TJVTl4uix5yaaIK/QI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB\nrjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug\nQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho\ndHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt\nQ2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa\nFw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV\nBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l\ndHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE\nAxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B\nYHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9\nhVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l\nL8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm\nSGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM\n1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws\n6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw\nOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50\naWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH\nAwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u\n7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0\nxtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ\nrfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim\neOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk\nUSeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB\nlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug\nQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho\ndHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt\nSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe\nMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v\nd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh\ncmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn\n0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ\nM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a\nMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd\noI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI\nDsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy\noUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD\nVR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0\ndHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy\nbDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF\nBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM\n//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli\nCE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE\nCJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t\n3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS\nKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f\nzGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi\nTkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW\nNWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV\nGx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh\nc3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy\nMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp\nemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X\nDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw\nFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg\nUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo\nYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5\nMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK\nVdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm\nFc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID\nAQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J\nh9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul\nuIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68\nDzFc6PLZ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4\nnN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO\n8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV\nojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb\nPG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2\n6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr\nn5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a\nqtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4\nwTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3\nns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs\npSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4\nE1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns\nYXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH\nMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y\naXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe\nFw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX\nMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj\nIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx\nKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s\neTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B\nAQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM\nHiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw\nDqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC\nAwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji\nnb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX\nrXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn\njBJ7xUS0rg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy\naVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s\nIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp\nZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\neSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV\nBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp\nZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu\nYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g\nQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt\nIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU\nJ92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO\nJxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY\nwZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o\nkoqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN\nqWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E\nSrg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe\nxbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u\n7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU\nsQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI\nsH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP\ncjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE\nBarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is\nI19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G\nCSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do\nlbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc\nAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ\nBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh\nc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy\nMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp\nemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X\nDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw\nFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg\nUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo\nYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5\nMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB\nAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4\npO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0\n13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID\nAQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk\nU01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i\nF6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY\noJ2daZH9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b\nN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t\nKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu\nkxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm\nCC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ\nXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu\nimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te\n2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe\nDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC\n/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p\nF4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt\nTxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL\nMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\nZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln\nbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\nU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\naXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp\nU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg\nSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln\nbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nIC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm\nGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve\nfLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ\naW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj\naHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW\nkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC\n4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga\nFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\nyjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\nU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\nZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\naG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL\nMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW\nZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln\nbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp\nU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y\naXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1\nnmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex\nt0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz\nSdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG\nBO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+\nrCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/\nNIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E\nBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH\nBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy\naXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv\nMzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE\np6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y\n5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK\nWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ\n4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N\nhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw\nCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl\ncmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu\nLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT\naWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD\nVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT\naWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ\nbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu\nIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg\nLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1\nGQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ\n+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd\nU6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm\nNxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY\nufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/\nky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1\nCtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq\ng6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm\nfjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c\n2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/\nbLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB\nvTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp\nU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W\nZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe\nFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX\nMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0\nIE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y\nIGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh\nbCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF\n9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH\nH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H\nLL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN\n/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT\nrJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud\nEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw\nWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs\nexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud\nDgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4\nsAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+\nseQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz\n4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+\nBxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR\nlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3\n7M2CYfE45k+XmCpajQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr\nMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl\ncm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv\nbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw\nCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h\ndGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l\ncmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h\n2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E\nlpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV\nZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq\n299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t\nvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL\ndXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD\nAgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF\nAAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR\nzCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3\nLBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd\n7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw\n++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt\n398znM/jra6O1I7mT1GvFpLgXPYHDw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx\nIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs\ncyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v\ndCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0\nMDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl\nbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD\nDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r\nWxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU\nDk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs\nHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj\nz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf\nSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl\nAgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG\nKGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P\nAQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j\nBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC\nVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX\nZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg\nUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB\nALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd\n/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB\nA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn\nk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9\niW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv\n2G0xffX8oRAHh84vWdw+WNs=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB\ngjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk\nMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY\nUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx\nNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3\ndy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy\ndmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB\ndXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6\n38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP\nKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q\nDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4\nqEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa\nJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi\nPvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P\nBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs\njVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0\neS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD\nggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR\nvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt\nqZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa\nIR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy\ni6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ\nO+7ETPTsJ3xCwnR8gooJybQDJbw=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIIDjCCBfagAwIBAgIJAOiOtsn4KhQoMA0GCSqGSIb3DQEBBQUAMIG8MQswCQYD\nVQQGEwJVUzEQMA4GA1UECBMHSW5kaWFuYTEVMBMGA1UEBxMMSW5kaWFuYXBvbGlz\nMSgwJgYDVQQKEx9Tb2Z0d2FyZSBpbiB0aGUgUHVibGljIEludGVyZXN0MRMwEQYD\nVQQLEwpob3N0bWFzdGVyMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkx\nJTAjBgkqhkiG9w0BCQEWFmhvc3RtYXN0ZXJAc3BpLWluYy5vcmcwHhcNMDgwNTEz\nMDgwNzU2WhcNMTgwNTExMDgwNzU2WjCBvDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT\nB0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMfU29mdHdh\ncmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1hc3RlcjEe\nMBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcNAQkBFhZo\nb3N0bWFzdGVyQHNwaS1pbmMub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\nCgKCAgEA3DbmR0LCxFF1KYdAw9iOIQbSGE7r7yC9kDyFEBOMKVuUY/b0LfEGQpG5\nGcRCaQi/izZF6igFM0lIoCdDkzWKQdh4s/Dvs24t3dHLfer0dSbTPpA67tfnLAS1\nfOH1fMVO73e9XKKTM5LOfYFIz2u1IiwIg/3T1c87Lf21SZBb9q1NE8re06adU1Fx\nY0b4ShZcmO4tbZoWoXaQ4mBDmdaJ1mwuepiyCwMs43pPx93jzONKao15Uvr0wa8u\njyoIyxspgpJyQ7zOiKmqp4pRQ1WFmjcDeJPI8L20QcgHQprLNZd6ioFl3h1UCAHx\nZFy3FxpRvB7DWYd2GBaY7r/2Z4GLBjXFS21ZGcfSxki+bhQog0oQnBv1b7ypjvVp\n/rLBVcznFMn5WxRTUQfqzj3kTygfPGEJ1zPSbqdu1McTCW9rXRTunYkbpWry9vjQ\nco7qch8vNGopCsUK7BxAhRL3pqXTT63AhYxMfHMgzFMY8bJYTAH1v+pk1Vw5xc5s\nzFNaVrpBDyXfa1C2x4qgvQLCxTtVpbJkIoRRKFauMe5e+wsWTUYFkYBE7axt8Feo\n+uthSKDLG7Mfjs3FIXcDhB78rKNDCGOM7fkn77SwXWfWT+3Qiz5dW8mRvZYChD3F\nTbxCP3T9PF2sXEg2XocxLxhsxGjuoYvJWdAY4wCAs1QnLpnwFVMCAwEAAaOCAg8w\nggILMB0GA1UdDgQWBBQ0cdE41xU2g0dr1zdkQjuOjVKdqzCB8QYDVR0jBIHpMIHm\ngBQ0cdE41xU2g0dr1zdkQjuOjVKdq6GBwqSBvzCBvDELMAkGA1UEBhMCVVMxEDAO\nBgNVBAgTB0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMf\nU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1h\nc3RlcjEeMBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcN\nAQkBFhZob3N0bWFzdGVyQHNwaS1pbmMub3JnggkA6I62yfgqFCgwDwYDVR0TAQH/\nBAUwAwEB/zARBglghkgBhvhCAQEEBAMCAAcwCQYDVR0SBAIwADAuBglghkgBhvhC\nAQ0EIRYfU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDAwBglghkgBhvhC\nAQQEIxYhaHR0cHM6Ly9jYS5zcGktaW5jLm9yZy9jYS1jcmwucGVtMDIGCWCGSAGG\n+EIBAwQlFiNodHRwczovL2NhLnNwaS1pbmMub3JnL2NlcnQtY3JsLnBlbTAhBgNV\nHREEGjAYgRZob3N0bWFzdGVyQHNwaS1pbmMub3JnMA4GA1UdDwEB/wQEAwIBBjAN\nBgkqhkiG9w0BAQUFAAOCAgEAtM294LnqsgMrfjLp3nI/yUuCXp3ir1UJogxU6M8Y\nPCggHam7AwIvUjki+RfPrWeQswN/2BXja367m1YBrzXU2rnHZxeb1NUON7MgQS4M\nAcRb+WU+wmHo0vBqlXDDxm/VNaSsWXLhid+hoJ0kvSl56WEq2dMeyUakCHhBknIP\nqxR17QnwovBc78MKYiC3wihmrkwvLo9FYyaW8O4x5otVm6o6+YI5HYg84gd1GuEP\nsTC8cTLSOv76oYnzQyzWcsR5pxVIBcDYLXIC48s9Fmq6ybgREOJJhcyWR2AFJS7v\ndVkz9UcZFu/abF8HyKZQth3LZjQl/GaD68W2MEH4RkRiqMEMVObqTFoo5q7Gt/5/\nO5aoLu7HaD7dAD0prypjq1/uSSotxdz70cbT0ZdWUoa2lOvUYFG3/B6bzAKb1B+P\n+UqPti4oOxfMxaYF49LTtcYDyeFIQpvLP+QX4P4NAZUJurgNceQJcHdC2E3hQqlg\ng9cXiUPS1N2nGLar1CQlh7XU4vwuImm9rWgs/3K1mKoGnOcqarihk3bOsPN/nOHg\nT7jYhkalMwIsJWE3KpLIrIF0aGOHM3a9BX9e1dUCbb2v/ypaqknsmHlHU5H2DjRa\nyaXG67Ljxay2oHA1u8hRadDytaIybrw/oDc5fHE2pgXfDBLkFqfF1stjo5VwP+YE\no2A=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE\nAwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw\nCQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ\nBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND\nVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb\nqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY\nHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo\nG2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA\nlHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr\nIA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/\n0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH\nk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47\n4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO\nm3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa\ncXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl\nuUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI\nKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls\nZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG\nAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2\nVuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT\nVfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG\nCCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA\ncgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA\nQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA\n7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA\ncgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA\nQwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA\nczAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu\naHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt\naW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud\nDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF\nBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp\nD70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU\nJyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m\nAM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD\nvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms\ntn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH\n7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h\nI6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA\nh1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF\nd3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H\npPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE\nBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w\nMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290\nIENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC\nSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1\nODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv\nUTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX\n4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9\nKK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/\ngCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb\nrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ\n51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F\nbe8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe\nKF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F\nv6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn\nfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7\njPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz\nezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt\nifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL\ne3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70\njsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz\nWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V\nSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j\npwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX\nX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok\nfcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R\nK4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU\nZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU\nLysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT\nLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE\nAwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG\nEwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM\nFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC\nREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp\nNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM\nVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+\nSZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ\n4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L\ncp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi\neowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV\nHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG\nA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3\nDQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j\nvZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP\nDpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc\nmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D\nlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv\nKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr\n6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV\nL4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91\n1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx\nMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ\nQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB\narcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr\nUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi\nFRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS\nP/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN\n9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz\nuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h\n9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s\nA20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t\nOluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo\n+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7\nKcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2\nDISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us\nH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ\nI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7\n5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h\n3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz\nY11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd\nMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg\nQ2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow\nTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw\nHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y\nZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E\nN3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9\ntznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX\n0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c\n/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X\nKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY\nzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS\nO1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D\n34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP\nK9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3\nAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv\nTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj\nQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV\ncSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS\nIGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2\nHJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa\nO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv\n033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u\ndmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE\nkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41\n3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD\nu79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq\n4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy\nMDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk\nD2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o\nOI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A\nfQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe\nIgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n\noc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK\n/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj\nrckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD\n3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE\n7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC\nyC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd\nqvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI\nhvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR\nxVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA\nSfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo\nHqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB\nemOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC\nAMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb\n7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x\nDzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk\nF7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF\na3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT\nQ6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV\nBAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu\nMRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy\nMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx\nEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe\nNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH\nPWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I\nx2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe\nQTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR\nyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO\nQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912\nH9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ\nQfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD\ni/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs\nnLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1\nrqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud\nDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI\nhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM\ntCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf\nGopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb\nlvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka\n+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal\nTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i\nnSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3\ngzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr\nG5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os\nzMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x\nL4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC\nQ04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g\nQ2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0\naW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa\nFw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg\nSW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo\naW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp\nZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z\n7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA//\nDdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx\nzUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8\nhBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs\n4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u\ngQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY\nNJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E\nFgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3\nj92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG\n52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB\nechNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws\nZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI\nzo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy\nwy39FCqQmbkHzJ8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\nb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG\nEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\ncnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA\nn61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc\nbiJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp\nEgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA\nbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu\nYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB\nAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW\nBBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI\nQW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I\n0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni\nlmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9\nB5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv\nON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo\nIhNzbM8m9Yop5w==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg\nRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq\nhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf\nZn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q\nRSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/\nBAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD\nAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY\nJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv\n6pZjamVFkpUBtA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\nMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\nMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\nb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\nq2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\ntCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\nvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\nBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\nNeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\nFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\npLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\nMrY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw\nCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu\nZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe\nFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw\nEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x\nIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF\nK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG\nfp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO\nZ9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd\nBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx\nAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/\noAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8\nsycX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\nd3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg\nRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV\nUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\nY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y\nithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If\nxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV\nySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO\nDCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ\njdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/\nCNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi\nEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM\nfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY\nuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK\nchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t\n9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD\nggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2\nSV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd\n+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc\nfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa\nsjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N\ncCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N\n0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie\n4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI\nr/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1\n/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm\ngKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha\nME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM\nHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03\nUAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42\ntSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R\nySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM\nlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp\n/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G\nA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj\ndG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy\nMENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl\ncmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js\nL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL\nBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni\nacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0\no3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K\nzCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8\nPIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y\nJohw1+qRzT65ysCQblrGXnRl11z+o+I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF\nMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD\nbGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw\nNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV\nBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn\nljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0\n3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z\nqQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR\np75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8\nHgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw\nggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea\nHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw\nOi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh\nc3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E\nRT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt\ndHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku\nY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp\n3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05\nnsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF\nCSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na\nxpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX\nKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB\n8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy\ndGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1\nYmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3\ndy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh\nIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD\nLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG\nEwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g\nKE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD\nZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu\nbmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg\nZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R\n85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm\n4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV\nHMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd\nQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t\nlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB\no4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E\nBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4\nopvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo\ndHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW\nZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN\nAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y\n/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k\nSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy\nRp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS\nAgu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl\nnJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy\nMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl\nZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS\nb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy\neuuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO\nbntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw\nWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d\nMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE\n1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/\nzQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB\nBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF\nBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV\nv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG\nE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u\nuSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW\niAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v\nGVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV\nBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC\naWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV\nBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1\nZ3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz\nMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+\nBgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp\nem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN\nZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY\nB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH\nD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF\nQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo\nq1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D\nk14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH\nfC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut\ndEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM\nti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8\nzLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn\nrFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX\nU8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6\nJyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5\nXPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF\nNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR\nHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY\nGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c\n77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3\n+GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK\nvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6\nFiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl\nyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P\nAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD\ny4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d\nNL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix\nRDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1\ndGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p\nYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw\nNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK\nEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl\ncnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl\nc2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz\ndYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ\nfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns\nbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD\n75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP\nFEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV\nHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp\n5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu\nb3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA\nA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p\n6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8\nTqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7\ndIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys\nNnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI\nl7WdmplNsDz4SgCbZN2fOUvRJ9e4\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1\ndG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s\nYW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz\ndHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0\naWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh\nIGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ\nKoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw\nMFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy\nb2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx\nKjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG\nA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u\naWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI\nhvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9\n7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74\nBCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G\nieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9\nJcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0\nPghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2\n0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH\n0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/\n6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m\nv6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7\nK2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev\nbqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw\nMC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w\nMB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD\ngBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0\nb3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh\nbm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0\ncml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp\nZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg\nZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq\nhkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD\nAgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w\nMDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag\nRKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t\nUkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl\ncnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v\nY3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG\nAQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN\nAQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS\n1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB\n3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv\nWb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh\nHVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm\npHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz\nsOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE\nqCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb\nmRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9\nopLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H\nYvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00\nMjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV\nwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe\nrNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341\n68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh\n4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp\nUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o\nabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc\n3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G\nKubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt\nhfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO\nTk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt\nzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD\nggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC\nMTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2\ncDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN\nqXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5\nYCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv\nb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2\n8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k\nNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj\nZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp\nq1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt\nnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00\nMjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf\nqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW\nn4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym\nc5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+\nO7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1\no9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j\nIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq\nIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz\n8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh\nvNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l\n7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG\ncC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD\nggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66\nAarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC\nroijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga\nW/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n\nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE\n+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV\ncsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd\ndbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg\nKCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM\nHVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4\nWSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL\nBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc\nBgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00\nMjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM\naW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR\n/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu\nFoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR\nU7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c\nra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR\nFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k\nA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw\neyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl\nsSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp\nVzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q\nA4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+\nydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB\nBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD\nggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px\nKGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI\nFUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv\noxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg\nu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP\n0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf\n3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl\n8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+\nDhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN\nPlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/\nywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl\nMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe\nU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX\nDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy\ndXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj\nYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV\nOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr\nzbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM\nVAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ\nhNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO\nojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw\nawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs\nOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\nDQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF\ncoJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc\nokgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8\nt/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy\n1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/\nSjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGGTCCBAGgAwIBAgIIPtVRGeZNzn4wDQYJKoZIhvcNAQELBQAwajEhMB8GA1UE\nAxMYU0cgVFJVU1QgU0VSVklDRVMgUkFDSU5FMRwwGgYDVQQLExMwMDAyIDQzNTI1\nMjg5NTAwMDIyMRowGAYDVQQKExFTRyBUUlVTVCBTRVJWSUNFUzELMAkGA1UEBhMC\nRlIwHhcNMTAwOTA2MTI1MzQyWhcNMzAwOTA1MTI1MzQyWjBqMSEwHwYDVQQDExhT\nRyBUUlVTVCBTRVJWSUNFUyBSQUNJTkUxHDAaBgNVBAsTEzAwMDIgNDM1MjUyODk1\nMDAwMjIxGjAYBgNVBAoTEVNHIFRSVVNUIFNFUlZJQ0VTMQswCQYDVQQGEwJGUjCC\nAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoVgLsfJXwTukK0rcHoyKL\nULO5Lhk9V9sZqtIr5M5C4myh5F0lHjMdtkXRtPpZilZwyW0IdmlwmubHnAgwE/7m\n0ZJoYT5MEfJu8rF7V1ZLCb3cD9lxDOiaN94iEByZXtaxFwfTpDktwhpz/cpLKQfC\neSnIyCauLMT8I8hL4oZWDyj9tocbaF85ZEX9aINsdSQePHWZYfrSFPipS7HYfad4\n0hNiZbXWvn5qA7y1svxkMMPQwpk9maTTzdGxxFOHe0wTE2Z/v9VlU2j5XB7ltP82\nmUWjn2LAfxGCAVTeD2WlOa6dSEyJoxA74OaD9bDaLB56HFwfAKzMq6dgZLPGxXvH\nVUZ0PJCBDkqOWZ1UsEixUkw7mO6r2jS3U81J2i/rlb4MVxH2lkwEeVyZ1eXkvm/q\nR+5RS+8iJq612BGqQ7t4vwt+tN3PdB0lqYljseI0gcSINTjiAg0PE8nVKoIV8IrE\nQzJW5FMdHay2z32bll0eZOl0c8RW5BZKUm2SOdPhTQ4/YrnerbUdZbldUv5dCamc\ntKQM2S9FdqXPjmqanqqwEaHrYcbrPx78ZrQSnUZ/MhaJvnFFr5Eh2f2Tv7QCkUL/\nSR/tixVo3R+OrJvdggWcRGkWZBdWX0EPSk8ED2VQhpOX7EW/XcIc3M/E2DrmeAXQ\nxVVVqV7+qzohu+VyFPcLAgMBAAGjgcIwgb8wHQYDVR0OBBYEFCkgy/HDD9oGjhOT\nh/5fYBopu/O2MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUKSDL8cMP2gaO\nE5OH/l9gGim787YwEQYDVR0gBAowCDAGBgRVHSAAMEkGA1UdHwRCMEAwPqA8oDqG\nOGh0dHA6Ly9jcmwuc2d0cnVzdHNlcnZpY2VzLmNvbS9yYWNpbmUtR3JvdXBlU0cv\nTGF0ZXN0Q1JMMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEATEZn\n4ERQ9cW2urJRCiUTHbfHiC4fuStkoMuTiFJZqmD1zClSF/8E5ze0MRFGfisebKeL\nPEeaXvSqXZA7RT2fSsmKe47A7j55i5KjyJRKuCgRa6YlX129x8j7g09VMeZc8BN8\n471/Kiw3N5RJr4QfFCeiWBCPCjk3GhIgQY8Z9qkfGe2yNLKtfTNEi18KB0PydkVF\nLa3kjQ4A/QQIqudr+xe9sAhWDjUqcvCz5006Tw3c82ASszhkjNv54SaNL+9O6CRH\nPjY0imkPKGuLh8a9hSb50+tpIVZgkdb34GLCqHGuLt5mI7VSRqakSDcsfwEWVxH3\nJw0O5Q/WkEXhHj8h3NL8FhgTPk1qsiZqQF4leP049KxYejcbmEAEx47J1MRnYbGY\nrvDNDty5r2WDewoEij9hqvddQYbmxkzCTzpcVuooO6dEz8hKZPVyYC3jQ7hK4HU8\nMuSqFtcRucFF2ZtmY2blIrc07rrVdC8lZPOBVMt33lfUk+OsBzE6PlwDg1dTx/D+\naNglUE0SyObhlY1nqzyTPxcCujjXnvcwpT09RAEzGpqfjtCf8e4wiHPvriQZupdz\nFcHscQyEZLV77LxpPqRtCRY2yko5isune8YdfucziMm+MG2chZUh6Uc7Bn6B4upG\n5nBYgOao8p0LadEziVkw82TTC/bOKwn7fRB2LhA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg\nQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh\ndGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9\nMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi\nU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh\ncnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA\nA4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk\npMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf\nOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C\nJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT\nKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi\nHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM\nAv+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w\n+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+\nGkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3\nZzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B\n26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID\nAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD\nVR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul\nF2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC\nATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\nZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk\naWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0\nYXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg\nc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0\naWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93\nd3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG\nCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1\ndGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF\nwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS\nTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst\n0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc\npRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl\nCcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF\nP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK\n1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm\nKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE\nJnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ\n8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm\nfyWl8kgAwKQB2j8=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW\nMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm\naWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1\nOTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG\nA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G\nCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ\nJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD\nvfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo\nD/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/\nQ0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW\nRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK\nHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN\nnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM\n0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i\nUUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9\nHa90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg\nTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE\nAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL\nBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K\n2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX\nUfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl\n6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK\n9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ\nHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI\nwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY\nXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l\nIxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo\nhdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr\nso8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk\nMQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0\nYWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg\nQ0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT\nAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp\nY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr\njw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r\n0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f\n2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP\nACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF\ny6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA\ntukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL\n6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0\nuPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL\nacywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh\nk6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q\nVAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw\nFDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O\nBBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh\nb97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R\nfbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv\n/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI\nREeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx\nsrpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv\naGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT\nwoCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n\nBjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W\nt6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N\n8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2\n9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5\nwSsSnqaeG8XmDtkx2Q==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw\nZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp\ndGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290\nIEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD\nVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy\ndGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg\nMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx\nUglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD\n1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH\noCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR\nHvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/\n5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv\nidm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL\nOdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC\nNYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f\n46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB\nUWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth\n7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G\nA1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED\nMB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB\nbj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x\nXCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T\nPLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0\nWqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70\nWBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL\nGn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm\n7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S\nnr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN\nvBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB\nWkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI\nfI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb\nI+2ksx0WckNLIOFZfsLorSa/ovc=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw\nNzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv\nb3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD\nVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F\nVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1\n7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X\nZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+\n/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\n81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm\ndtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe\nOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu\nsDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4\npgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs\nslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ\narMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD\nVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG\n9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\ndxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx\n0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj\nTQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed\nY2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7\nQ4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI\nOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7\nvVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW\nt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn\nHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\nSK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF\nMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL\nExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx\nMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc\nMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+\nAOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH\niTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj\nvSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA\n0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB\nOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/\nBAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E\nFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01\nGX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW\nzaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4\n1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE\nf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F\njZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN\nZetX2fNXlrtIzYE=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd\nAqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC\nFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi\n1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq\njnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ\nwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/\nWSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy\nNsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC\nuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw\nIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6\ng1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN\n9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP\nBSeOE6Fuwg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx\nKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd\nBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl\nYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1\nOTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy\naXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50\nZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN\n8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/\nRLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4\nhqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5\nZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM\nEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj\nQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1\nA/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy\nWL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ\n1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30\n6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT\n91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml\ne9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p\nTpPDpFQUWw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc\nUktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx\nc8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS\nS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg\nSGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx\nOVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry\nb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC\nVFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE\nsGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F\nni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY\nKTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG\n+7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG\nHtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P\nIzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M\n733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk\nYb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G\nCSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW\nAkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I\naE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5\nmxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa\nXRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ\nqxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx\nEjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT\nVFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5\nNTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT\nB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG\nSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF\n10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz\n0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh\nMBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH\nzIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc\n46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2\nyKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi\nlaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP\noA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA\nBDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE\nqYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm\n4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB\n/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL\n1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn\nLhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF\nH6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo\nRI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+\nnile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh\n15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW\n6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW\nnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j\nwa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz\naGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy\nKwbQBM0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG\nA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz\ncyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2\nMDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV\nBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt\nYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN\nADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE\nBarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is\nI19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G\nCSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i\n2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ\n2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG\nMQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV\nBAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw\nMTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl\nZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r\nD195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1\n9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf\nv5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk\nUkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L\nNVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb\n+gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V\nqyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K\nyX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G\nAbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK\nJ/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC\nAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4\nWbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6\nyAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj\n/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6\njBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2\nltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX\nX0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n\nFoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D\nu9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l\nO1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le\nie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1\n2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV\nMQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV\nBAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw\nMTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX\nb1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp\ndHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN\nrLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U\nfcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc\nf+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2\nZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M\nx1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR\naG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch\nzDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar\nuHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K\nmYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA\nSh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv\nHYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H\nEtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1\nLOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ\nMuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e\nJXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN\ng64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp\ndIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab\nR80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ\nPkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce\nxGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+\nJ7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl\nOtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT\nee5Ehr7XHuQe+w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "build/homebrew.sh",
    "content": "#!/bin/bash\n#\n# homebrew.sh creates an updated homebrew on fabio\n\nset -o nounset\nset -o errexit\nset -o pipefail\n\nreadonly prgdir=$(cd $(dirname $0); pwd)\nreadonly brewdir=$(brew tap-info homebrew/core | head -n 2 | tail -n 1 | sed 's/[[:space:]].*$//')\n\nv=${1:-}\n[[ -n \"$v\" ]] || read -p \"Enter version (e.g. 1.0.4): \" v\nif [[ -z \"$v\" ]] ; then\n\techo \"Usage: $0 <version> (e.g. 1.0.4)\"\n\texit 1\nfi\nv=${v/v/}\n\nsrcurl=https://github.com/fabiolb/fabio/archive/v${v}.tar.gz\nshasum=$(wget -O- -q \"$srcurl\" | shasum -a 256 | awk '{ print $1; }')\necho -e \"/url\nDAurl \\\"$srcurl\\\"\u001b/sha256\nDAsha256 \\\"$shasum\\\"\u001b:wq\" > $prgdir/homebrew.vim\n\nbrew update\nbrew update\n(\n\tcd $brewdir\n\tgit checkout -b fabio-$v origin/master\n\tvim -u NONE -s $prgdir/homebrew.vim $brewdir/Formula/fabio.rb\n\tbrew install --build-from-source fabio\n\tbrew test fabio\n\tbrew install fabio\n\tbrew audit --strict fabio\n\tgit add Formula/fabio.rb\n\tgit commit -m \"fabio $v\"\n\tgit push --set-upstream fabiolb fabio-$v\n)\n\necho \"Goto https://github.com/fabiolb/homebrew-core to create pull request\"\nopen https://github.com/fabiolb/homebrew-core\n\nexit 0\n"
  },
  {
    "path": "build/homebrew.vim",
    "content": "/url\nDAurl \"https://github.com/fabiolb/fabio/archive/v1.6.3.tar.gz\"\u001b/sha256\nDAsha256 \"e85b70a700652b051260b8c49ce63d21d2579517601a91d893a7fa9444635ad3\"\u001b:wq\n"
  },
  {
    "path": "build/issue-225-gen-cert.bash",
    "content": "#!/bin/bash\n#\n# This script addresses issue #225 (https://github.com/fabiolb/fabio/issues/225)\n# and generates a number of certificates for testing fabio with client\n# certificate authentication with signed client certificates.\n#\n# First, a self-signed CA certificate is created which is used to sign both the\n# server and client certificates. Then a server and a client certificate are\n# created. The demo/cert/{ca,client,server} directories contain the generated\n# certificates and their private keys.\n#\n# Second, a directory structure for a fabio path cert store is created under\n# demo/cert/fabio/{client,server}. The server directory contains the TLS server\n# certificate and private key. The client directory contains the client\n# certificate **and** the CA certificate (no private keys). Including the CA\n# certificate is necessary since otherwise fabio (or the go crypto library)\n# cannot verify the client certificate and will respond with the following\n# error. Try this by removing the demo/cert/fabio/client/ca-cert.pem file and\n# restart fabio.\n#\n# http: TLS handshake error from 127.0.0.1:53272: tls: failed to verify client's certificate: x509: certificate signed by unknown authority\n#\n\nset -o errexit\nset -o nounset\n\nbasedir=$(cd $(dirname $0)/.. ; pwd)\ncertdir=$basedir/demo/cert\nopenssl=$(which openssl)\n[[ -x /usr/local/opt/openssl/bin/openssl ]] && openssl=/usr/local/opt/openssl/bin/openssl\n\n# shorten certdir\ncertdir=${certdir/$(pwd)\\//}\n\n[[ -z \"$certdir\" ]] && (echo \"certdir empty\" ; exit 1)\n[[ -d \"$certdir\" ]] && rm -rf \"$certdir\"\nmkdir -p $certdir/{ca,client,server} $certdir/fabio/{client,server}\n\necho \"generate CA cert\"\n$openssl req \\\n\t-x509 -nodes -days 365 -sha256 -newkey rsa:2048 \\\n\t-subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=ca' \\\n\t-keyout \"$certdir/ca/ca-key.pem\" -out \"$certdir/ca/ca-cert.pem\"\n\necho \"generate client cert\"\n$openssl req \\\n\t-nodes -days 365 -sha256 -newkey rsa:2048 \\\n\t-subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=client' \\\n\t-keyout $certdir/client/client-key.pem -out $certdir/client/client.csr\n\n$openssl x509 \\\n\t-req -set_serial 02 -CA $certdir/ca/ca-cert.pem -CAkey $certdir/ca/ca-key.pem \\\n\t-in $certdir/client/client.csr -out $certdir/client/client-cert.pem\n\necho \"generate server cert\"\n$openssl req \\\n\t-nodes -days 365 -sha256 -newkey rsa:2048 \\\n\t-subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=www.server.com' \\\n\t-keyout $certdir/server/server-key.pem -out $certdir/server/server.csr\n\n$openssl x509 \\\n\t-req -set_serial 03 -CA $certdir/ca/ca-cert.pem -CAkey $certdir/ca/ca-key.pem \\\n\t-in $certdir/server/server.csr -out $certdir/server/server-cert.pem\n\ncp $certdir/ca/ca-cert.pem $certdir/fabio/client\ncp $certdir/client/client-cert.pem $certdir/fabio/client\ncp $certdir/server/server-{cert,key}.pem $certdir/fabio/server\n\ncat<<EOF\n\n# start fabio with path cert store\n$basedir/fabio \\\\\n -proxy.cs 'cs=db;type=path;refresh=3s;cert=$certdir/fabio/server;clientca=$certdir/fabio/client' \\\\\n -proxy.addr ':9999;cs=db'\n\n# connect with openssl and client cert\n$openssl s_client \\\\\n -tls1_2 -CAfile $certdir/ca/ca-cert.pem \\\\\n -servername www.server.com -connect localhost:9999 \\\\\n -cert $certdir/client/client-cert.pem -key $certdir/client/client-key.pem\n\nEOF\n"
  },
  {
    "path": "build/releasenotes.pl",
    "content": "#!/usr/bin/env perl\n\nuse strict;\n\nlocal $/;\n$_ = <>;\nif (/^### \\[$ENV{RELEASE}.*?\\n\\s*(.*?)^### \\[v/gms) {\n    print $1;\n}\n"
  },
  {
    "path": "build/tag.sh",
    "content": "#!/bin/bash -e\n#\n# Script for replacing the version number\n# in main.go, committing and tagging the code\n\nreadonly prgdir=$(cd $(dirname $0); pwd)\nreadonly basedir=$(cd $prgdir/..; pwd)\nv=$1\n\n[[ -n \"$v\" ]] || read -p \"Enter version (e.g. 1.0.4): \" v\nif [[ -z \"$v\" ]]; then\n\techo \"Usage: $0 <version> <remote>\"\n\texit 1\nfi\n\ngrep -q \"$v\" CHANGELOG.md || echo \"CHANGELOG.md not updated\"\n\nread -p \"Tag fabio version $v? (y/N) \" -n 1 -r\necho\nif [[ ! $REPLY =~ ^[Yy]$ ]]; then\n\texit 1\nfi\n\nsed -i '' -e \"s|^var version .*$|var version = \\\"$v\\\"|\" $basedir/main.go\ngit add $basedir/main.go\ngit commit -S -m \"Release v$v\" || true\ngit tag -s v$v -m \"Tag v${v}\"\n\nremote=$2\n\n[[ -n \"$origin\" ]] || read -p \"Enter remote (e.g. origin): \" origin\n\nif [[ -z \"$origin\" ]]; then\n\techo \"Usage: $0 <version> <remote>\"\n\texit 1\nfi\n\ngit push $remote $version\n"
  },
  {
    "path": "build/update-ssl.sh",
    "content": "#!/bin/bash -e \n#\n# Script for updating ca-certificates.crt\n\nprgdir=$(cd $(dirname $0) ; pwd)\n\necho \"Updating certificates\"\nsudo update-ca-certificates\ncp /etc/ssl/certs/ca-certificates.crt $prgdir\n"
  },
  {
    "path": "cert/consul_source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"path\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// ConsulSource implements a certificate source which loads\n// TLS and client authentication certificates from the consul KV store.\n// The CertURL/ClientCAURL must point to the base path of the certificates.\n// The TLS certificates are updated automatically when the KV store\n// changes.\ntype ConsulSource struct {\n\tCertURL     string\n\tClientCAURL string\n\tCAUpgradeCN string\n}\n\nfunc parseConsulURL(rawurl string) (config *api.Config, key string, err error) {\n\tif rawurl == \"\" || !strings.HasPrefix(rawurl, \"http://\") && !strings.HasPrefix(rawurl, \"https://\") {\n\t\treturn nil, \"\", errors.New(\"invalid url\")\n\t}\n\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tconfig = &api.Config{Address: u.Host, Scheme: u.Scheme}\n\tif len(u.Query()[\"token\"]) > 0 {\n\t\tconfig.Token = u.Query()[\"token\"][0]\n\t}\n\n\t// path needs to point to kv store and we need\n\t// to strip the prefix off to get the key\n\tconst prefix = \"/v1/kv/\"\n\tkey = u.Path\n\tif !strings.HasPrefix(key, prefix) {\n\t\treturn nil, \"\", errors.New(\"missing prefix: \" + prefix)\n\t}\n\tkey = key[len(prefix):]\n\treturn\n}\n\nfunc (s ConsulSource) LoadClientCAs() (*x509.CertPool, error) {\n\tif s.ClientCAURL == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tconfig, key, err := parseConsulURL(s.ClientCAURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := api.NewClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tload := func(key string) (map[string][]byte, error) {\n\t\tpemBlocks, _, err := getCerts(client, key, 0)\n\t\treturn pemBlocks, err\n\t}\n\treturn newCertPool(key, s.CAUpgradeCN, load)\n}\n\nfunc (s ConsulSource) Certificates() chan []tls.Certificate {\n\tif s.CertURL == \"\" {\n\t\treturn nil\n\t}\n\n\tconfig, key, err := parseConsulURL(s.CertURL)\n\tif err != nil {\n\t\tlog.Printf(\"[ERROR] cert: Failed to parse consul url. %s\", err)\n\t}\n\n\tclient, err := api.NewClient(config)\n\tif err != nil {\n\t\tlog.Printf(\"[ERROR] cert: Failed to create consul client. %s\", err)\n\t}\n\n\tpemBlocksCh := make(chan map[string][]byte, 1)\n\tgo watchKV(client, key, pemBlocksCh)\n\n\tch := make(chan []tls.Certificate, 1)\n\tgo func() {\n\t\tfor pemBlocks := range pemBlocksCh {\n\t\t\tcerts, err := loadCertificates(pemBlocks)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[ERROR] cert: Failed to load certificates. %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tch <- certs\n\t\t}\n\t}()\n\treturn ch\n}\n\n// watchKV monitors a key in the KV store for changes.\nfunc watchKV(client *api.Client, key string, pemBlocks chan map[string][]byte) {\n\tvar lastIndex uint64\n\tvar lastValue map[string][]byte\n\n\tfor {\n\t\tvalue, index, err := getCerts(client, key, lastIndex)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] cert: Error fetching certificates from %s. %v\", key, err)\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\n\t\tif !reflect.DeepEqual(value, lastValue) || index != lastIndex {\n\t\t\tlog.Printf(\"[DEBUG] cert: Certificate index changed to #%d\", index)\n\t\t\tpemBlocks <- value\n\t\t\tlastValue, lastIndex = value, index\n\t\t}\n\t}\n}\n\nfunc getCerts(client *api.Client, key string, waitIndex uint64) (pemBlocks map[string][]byte, lastIndex uint64, err error) {\n\tq := &api.QueryOptions{RequireConsistent: true, WaitIndex: waitIndex}\n\tkvpairs, meta, err := client.KV().List(key, q)\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"consul: list: %s\", err)\n\t}\n\tif len(kvpairs) == 0 {\n\t\treturn nil, meta.LastIndex, nil\n\t}\n\tpemBlocks = map[string][]byte{}\n\tfor _, kvpair := range kvpairs {\n\t\tpemBlocks[path.Base(kvpair.Key)] = kvpair.Value\n\t}\n\treturn pemBlocks, meta.LastIndex, nil\n}\n"
  },
  {
    "path": "cert/consul_source_test.go",
    "content": "package cert\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\nfunc TestParseConsulURL(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tin     string\n\t\tconfig *api.Config\n\t\tkey    string\n\t\terrstr string\n\t}{\n\t\t{\n\t\t\tname:   \"empty url\",\n\t\t\terrstr: \"invalid url\",\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid url\",\n\t\t\tin:     \"this is not a url\",\n\t\t\terrstr: \"invalid url\",\n\t\t},\n\t\t{\n\t\t\tname:   \"no kv store url\",\n\t\t\tin:     \"http://localhost:8500/path/to/cert\",\n\t\t\terrstr: \"missing prefix: /v1/kv/\",\n\t\t},\n\t\t{\n\t\t\tname:   \"url without token\",\n\t\t\tin:     \"http://localhost:8500/v1/kv/path/to/cert\",\n\t\t\tconfig: &api.Config{Address: \"localhost:8500\", Scheme: \"http\"},\n\t\t\tkey:    \"path/to/cert\",\n\t\t},\n\t\t{\n\t\t\tname:   \"https url\",\n\t\t\tin:     \"https://localhost:8500/v1/kv/path/to/cert\",\n\t\t\tconfig: &api.Config{Address: \"localhost:8500\", Scheme: \"https\"},\n\t\t\tkey:    \"path/to/cert\",\n\t\t},\n\t\t{\n\t\t\tname:   \"url with token\",\n\t\t\tin:     \"http://localhost:8500/v1/kv/path/to/cert?token=123\",\n\t\t\tconfig: &api.Config{Address: \"localhost:8500\", Scheme: \"http\", Token: \"123\"},\n\t\t\tkey:    \"path/to/cert\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconfig, key, err := parseConsulURL(tt.in)\n\t\t\tvar errstr string\n\t\t\tif err != nil {\n\t\t\t\terrstr = err.Error()\n\t\t\t}\n\t\t\tif got, want := errstr, tt.errstr; got != want {\n\t\t\t\tt.Fatalf(\"got err %q want %q\", got, want)\n\t\t\t}\n\t\t\tif errstr != \"\" || tt.errstr != \"\" {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got, want := key, tt.key; got != want {\n\t\t\t\tt.Errorf(\"got key %q want %q\", got, want)\n\t\t\t}\n\t\t\tif got, want := config, tt.config; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got config %+v want %+v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cert/file_source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"os\"\n\n\t\"github.com/fabiolb/fabio/exit\"\n)\n\n// FileSource implements a certificate source for one\n// TLS and one client authentication certificate.\n// The certificates are loaded during startup and are cached\n// in memory until the program exits.\n// It exists to support the legacy configuration only. The\n// PathSource should be used instead.\ntype FileSource struct {\n\tCertFile       string\n\tKeyFile        string\n\tClientAuthFile string\n\tCAUpgradeCN    string\n}\n\nfunc (s FileSource) LoadClientCAs() (*x509.CertPool, error) {\n\treturn newCertPool(s.ClientAuthFile, s.CAUpgradeCN, func(path string) (map[string][]byte, error) {\n\t\tif s.ClientAuthFile == \"\" {\n\t\t\treturn nil, nil\n\t\t}\n\t\tpemBlock, err := os.ReadFile(path)\n\t\treturn map[string][]byte{path: pemBlock}, err\n\t})\n}\n\nfunc (s FileSource) Certificates() chan []tls.Certificate {\n\tch := make(chan []tls.Certificate, 1)\n\tch <- []tls.Certificate{loadX509KeyPair(s.CertFile, s.KeyFile)}\n\tclose(ch)\n\treturn ch\n}\n\nfunc loadX509KeyPair(certFile, keyFile string) tls.Certificate {\n\tif certFile == \"\" {\n\t\texit.Fatalf(\"[FATAL] cert: CertFile is required\")\n\t}\n\n\tif keyFile == \"\" {\n\t\tkeyFile = certFile\n\t}\n\n\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n\tif err != nil {\n\t\texit.Fatalf(\"[FATAL] cert: Error loading certificate. %s\", err)\n\t}\n\treturn cert\n}\n"
  },
  {
    "path": "cert/http_source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"time\"\n)\n\n// HTTPSource implements a certificate source which loads\n// TLS and client authentication certificates from an HTTP/HTTPS server.\n// The CertURL/ClientCAURL must point to a text file in the directory\n// of the certificates. The text file contains all files that should\n// be loaded from this directory - one filename per line.\n// The TLS certificates are updated automatically when Refresh\n// is not zero. Refresh cannot be less than one second to prevent\n// busy loops.\ntype HTTPSource struct {\n\tCertURL     string\n\tClientCAURL string\n\tCAUpgradeCN string\n\tRefresh     time.Duration\n}\n\nfunc (s HTTPSource) LoadClientCAs() (*x509.CertPool, error) {\n\treturn newCertPool(s.ClientCAURL, s.CAUpgradeCN, loadURL)\n}\n\nfunc (s HTTPSource) Certificates() chan []tls.Certificate {\n\tch := make(chan []tls.Certificate, 1)\n\tgo watch(ch, s.Refresh, s.CertURL, loadURL)\n\treturn ch\n}\n"
  },
  {
    "path": "cert/load.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// MaxSize is the limit for individual certificate sizes.\nconst MaxSize = 1 << 20 // 1MB\n\nfunc loadURL(listURL string) (pemBlocks map[string][]byte, err error) {\n\tif listURL == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tbaseURL, err := base(listURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cert: %s\", err)\n\t}\n\n\tfetch := func(url string) (buf []byte, err error) {\n\t\tresp, err := http.Get(url)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\treturn io.ReadAll(resp.Body)\n\t}\n\n\t// fetch the file with the list of filenames\n\tlist, err := fetch(listURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cert: %s\", err)\n\t}\n\n\t// fetch the individual files\n\tpemBlocks = map[string][]byte{}\n\tfor _, p := range strings.Split(string(list), \"\\n\") {\n\t\tif p == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := baseURL + p\n\n\t\tbuf, err := fetch(path)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cert: %s\", err)\n\t\t}\n\n\t\tpemBlocks[path] = buf\n\t}\n\n\treturn pemBlocks, nil\n}\n\nfunc loadPath(root string) (pemBlocks map[string][]byte, err error) {\n\tif root == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tpemBlocks = map[string][]byte{}\n\terr = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {\n\t\t// check if the root directory exists\n\t\tif _, ok := err.(*os.PathError); ok && path == root {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif info.IsDir() || filepath.Ext(info.Name()) != \".pem\" || strings.HasPrefix(info.Name(), \".\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif info.Size() > MaxSize {\n\t\t\tlog.Printf(\"[WARN] cert: File too large %s\", info.Name())\n\t\t\treturn nil\n\t\t}\n\n\t\tbuf, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cert: %s\", err)\n\t\t}\n\n\t\tpemBlocks[path] = buf\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pemBlocks, nil\n}\n\nfunc loadCertificates(pemBlocks map[string][]byte) ([]tls.Certificate, error) {\n\tvar errs []error\n\tvar n []string\n\tx := map[string]tls.Certificate{}\n\n\tfor name := range pemBlocks {\n\t\tvar certFile, keyFile string\n\t\tswitch {\n\t\tcase strings.HasSuffix(name, \"-cert.pem\"):\n\t\t\tcertFile, keyFile = name, replaceSuffix(name, \"-cert.pem\", \"-key.pem\")\n\t\tcase strings.HasSuffix(name, \"-key.pem\"):\n\t\t\tcertFile, keyFile = replaceSuffix(name, \"-key.pem\", \"-cert.pem\"), name\n\t\tcase strings.HasSuffix(name, \".pem\"):\n\t\t\tcertFile, keyFile = name, name\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, exists := x[certFile]; exists {\n\t\t\tcontinue\n\t\t}\n\n\t\tcert, key := pemBlocks[certFile], pemBlocks[keyFile]\n\t\tif cert == nil || key == nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"cert: cannot load certificate %s\", name))\n\t\t\tcontinue\n\t\t}\n\n\t\tc, err := tls.X509KeyPair(cert, key)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"cert: invalid certificate %s. %s\", name, err))\n\t\t\tcontinue\n\t\t}\n\n\t\tx[certFile] = c\n\t\tn = append(n, certFile)\n\t}\n\n\t// append certificates in alphabetical order of the\n\t// cert filenames. This determines which certificate\n\t// becomes the default certificate (the first one)\n\tsort.Strings(n)\n\tvar certs []tls.Certificate\n\tfor _, certFile := range n {\n\t\tcerts = append(certs, x[certFile])\n\t}\n\n\treturn certs, errors.Join(errs...)\n}\n\n// base returns the rawurl with the last element of the path\n// removed. http://foo.com/x/y becomes http://foo.com/x\nfunc base(rawurl string) (string, error) {\n\tif rawurl == \"\" {\n\t\treturn \"\", nil\n\t}\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif u.Path != \"/\" {\n\t\tu.Path = path.Dir(u.Path)\n\t}\n\treturn u.String(), nil\n}\n\n// replaceSuffix replaces oldSuffix with newSuffix in s.\n// It is only valid when s has oldSuffix and oldSuffix is not empty.\nfunc replaceSuffix(s string, oldSuffix, newSuffix string) string {\n\treturn s[:len(s)-len(oldSuffix)] + newSuffix\n}\n\n// newCertPool creates a new x509.CertPool by loading the\n// PEM blocks from loadFn(path) and adding them to a CertPool.\nfunc newCertPool(path string, caUpgradeCN string, loadFn func(path string) (pemBlocks map[string][]byte, err error)) (*x509.CertPool, error) {\n\tpemBlocks, err := loadFn(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(pemBlocks) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tpool := x509.NewCertPool()\n\tfor _, pemBlock := range pemBlocks {\n\t\tfor p, rest := pem.Decode(pemBlock); p != nil; p, rest = pem.Decode(rest) {\n\t\t\tcert, err := x509.ParseCertificate(p.Bytes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tupgradeCACertificate(cert, caUpgradeCN)\n\t\t\tpool.AddCert(cert)\n\t\t}\n\t}\n\n\tlog.Printf(\"[INFO] cert: Load client CA certs from %s\", path)\n\treturn pool, nil\n}\n\n// upgradeCACertificate upgrades a certificate to a self-signing CA certificate if the CN matches.\n// Issue #108: Allow generated AWS API Gateway certs to be used for client cert authentication\nfunc upgradeCACertificate(cert *x509.Certificate, caUpgradeCN string) {\n\tif caUpgradeCN != \"\" && caUpgradeCN == cert.Issuer.CommonName {\n\t\tcert.BasicConstraintsValid = true\n\t\tcert.IsCA = true\n\t\tcert.KeyUsage = x509.KeyUsageCertSign\n\t\tlog.Printf(\"[INFO] cert: Upgrading cert %s to CA cert\", cert.Issuer.CommonName)\n\t}\n}\n"
  },
  {
    "path": "cert/load_test.go",
    "content": "package cert\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"testing\"\n)\n\nfunc TestBase(t *testing.T) {\n\ttests := []struct {\n\t\tin, out, err string\n\t}{\n\t\t{\"\", \"\", \"\"},\n\t\t{\"http://foo.com/x/y\", \"http://foo.com/x\", \"\"},\n\t\t{\"http://foo.com/x/y?p=q\", \"http://foo.com/x?p=q\", \"\"},\n\t}\n\n\tfor i, tt := range tests {\n\t\tu, err := base(tt.in)\n\t\tif err != nil {\n\t\t\tif got, want := err.Error(), tt.err; got != want {\n\t\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif tt.err != \"\" {\n\t\t\tt.Errorf(\"%d: got nil want %v\", i, tt.err)\n\t\t\tcontinue\n\t\t}\n\t\tif got, want := u, tt.out; got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestReplaceSuffix(t *testing.T) {\n\tif got, want := replaceSuffix(\"ab\", \"b\", \"c\"), \"ac\"; got != want {\n\t\tt.Errorf(\"got %q want %q\", got, want)\n\t}\n}\n\nfunc TestUpgradeCACertificate(t *testing.T) {\n\t// generated at\n\t// https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/client-certificates\n\tconst awsAPIGWCert = `\n-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIIZAgycYqDRqQwDQYJKoZIhvcNAQELBQAwNDELMAkGA1UE\nBhMCVVMxEDAOBgNVBAcTB1NlYXR0bGUxEzARBgNVBAMTCkFwaUdhdGV3YXkwHhcN\nMTYwNzEyMTkzMTMwWhcNMTcwNzEyMTkzMTMwWjA0MQswCQYDVQQGEwJVUzEQMA4G\nA1UEBxMHU2VhdHRsZTETMBEGA1UEAxMKQXBpR2F0ZXdheTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAM/a0LKQd/obIwcKu09EjlHP4b7QqmK/JnJfd1Eq\nm6We85FGu+26s7+Bpw1xiyK2jFuzQ4JFyXVkWLJH8e3Mp7P91MvJ1x6UCRk+Fz6Q\nLauw5SBVmDO5CauB4NcICTYEeTT3c0m8t6sDpHf+DHZC87gq9rhBggKXfNO3ntWw\nKq2uGscvnOz2/n2XIucFf2U7GI/cOapGXvIyrB5e/swSCyNkgOJ2HekzWjprxSs5\nzu9JSOIzejgm8+/nnPOO9ycVrjN3qazUEXfF1QdvZeNCZ9GL6ZICAYo9xnnNLJnW\n6p5d0Fw6U+V/nlNpgCB5djTwXaY51ScoW/i3ukHBZe9QIEcCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEAzwUJlSv/9XVoeCbot+3mdviZI5B7VnEKGl2Oam1fQzGZkkzB\nkqBgtRrHux3BRxPRqS4jM4akdplFhejHExVatOxfS+DEXzFefi+aMb7qApB1YjV/\n5FIIQdZaVOlw2KIRXCy04nxrKJmJ1T5RCkYC80dYpNfmDb5REUtp8jU78/Schsx7\n0nCsrWkBSO1QtR4NnBlHbEM+imh3aCQz23SUK5Q/NTe4r2pu0zUl5b2YNgefvWle\n7fe6T137rmhji9K+tYNznLGk0XmiguQPM2qJLxqeVQsA32wUbbSIFWH+KsXRPfpU\nn/iFVG4Y6zyXQY2RzTt+ZB2VPR72X4wqS9fBeQ==\n-----END CERTIFICATE-----`\n\n\tp, rest := pem.Decode([]byte(awsAPIGWCert))\n\tif len(rest) > 0 {\n\t\tt.Fatal(\"want only one cert\")\n\t}\n\tcert, err := x509.ParseCertificate(p.Bytes)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check that cert does not have the flags set\n\tif got, want := cert.BasicConstraintsValid, false; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.IsCA, false; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.KeyUsage, x509.KeyUsage(0); got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\n\t// run upgrade with not-matching CN expecting no change\n\tupgradeCACertificate(cert, \"no match\")\n\tif got, want := cert.BasicConstraintsValid, false; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.IsCA, false; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.KeyUsage, x509.KeyUsage(0); got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\n\t// run upgrade with matching CN\n\tupgradeCACertificate(cert, \"ApiGateway\")\n\tif got, want := cert.BasicConstraintsValid, true; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.IsCA, true; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := cert.KeyUsage, x509.KeyUsageCertSign; got != want {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "cert/path_source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\nconst (\n\tDefaultCertPath     = \"cert\"\n\tDefaultClientCAPath = \"clientca\"\n)\n\ntype PathSource struct {\n\tPath         string\n\tCertPath     string\n\tClientCAPath string\n\tCAUpgradeCN  string\n\tRefresh      time.Duration\n}\n\nfunc (s PathSource) LoadClientCAs() (*x509.CertPool, error) {\n\tpath := makePath(s.Path, s.ClientCAPath, DefaultClientCAPath)\n\treturn newCertPool(path, s.CAUpgradeCN, loadPath)\n}\n\nfunc (s PathSource) Certificates() chan []tls.Certificate {\n\tpath := makePath(s.Path, s.CertPath, DefaultCertPath)\n\tch := make(chan []tls.Certificate, 1)\n\tgo watch(ch, s.Refresh, path, loadPath)\n\treturn ch\n}\n\nfunc makePath(parent, child, defaultChild string) string {\n\tif child == \"\" {\n\t\treturn filepath.Join(parent, defaultChild)\n\t}\n\treturn filepath.Join(parent, child)\n}\n"
  },
  {
    "path": "cert/source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"golang.org/x/sync/singleflight\"\n)\n\n// Source provides the interface for dynamic certificate sources.\ntype Source interface {\n\t// Certificates() loads certificates for TLS connections.\n\t// The first certificate is used as the default certificate\n\t// if the client does not support SNI or no matching certificate\n\t// could be found. TLS certificates can be updated at runtime.\n\tCertificates() chan []tls.Certificate\n\n\t// LoadClientCAs() provides certificates for client certificate\n\t// authentication.\n\tLoadClientCAs() (*x509.CertPool, error)\n}\n\n// Issuer is the interface implemented by sources that can issue certificates\n// on-demand.\ntype Issuer interface {\n\t// Issue issues a new certificate for the given common name. Issue must\n\t// return a certificate or an error, never (nil, nil).\n\tIssue(commonName string) (*tls.Certificate, error)\n}\n\n// NewSource generates a cert source from the config options.\nfunc NewSource(cfg config.CertSource) (Source, error) {\n\tswitch cfg.Type {\n\tcase \"file\":\n\t\treturn FileSource{\n\t\t\tCertFile:       cfg.CertPath,\n\t\t\tKeyFile:        cfg.KeyPath,\n\t\t\tClientAuthFile: cfg.ClientCAPath,\n\t\t\tCAUpgradeCN:    cfg.CAUpgradeCN,\n\t\t}, nil\n\n\tcase \"path\":\n\t\treturn PathSource{\n\t\t\tCertPath:     cfg.CertPath,\n\t\t\tClientCAPath: cfg.ClientCAPath,\n\t\t\tCAUpgradeCN:  cfg.CAUpgradeCN,\n\t\t\tRefresh:      cfg.Refresh,\n\t\t}, nil\n\n\tcase \"http\":\n\t\treturn HTTPSource{\n\t\t\tCertURL:     cfg.CertPath,\n\t\t\tClientCAURL: cfg.ClientCAPath,\n\t\t\tCAUpgradeCN: cfg.CAUpgradeCN,\n\t\t\tRefresh:     cfg.Refresh,\n\t\t}, nil\n\n\tcase \"consul\":\n\t\treturn ConsulSource{\n\t\t\tCertURL:     cfg.CertPath,\n\t\t\tClientCAURL: cfg.ClientCAPath,\n\t\t\tCAUpgradeCN: cfg.CAUpgradeCN,\n\t\t}, nil\n\n\tcase \"vault\":\n\t\treturn &VaultSource{\n\t\t\tCertPath:     cfg.CertPath,\n\t\t\tClientCAPath: cfg.ClientCAPath,\n\t\t\tCAUpgradeCN:  cfg.CAUpgradeCN,\n\t\t\tRefresh:      cfg.Refresh,\n\t\t\tClient:       NewVaultClient(cfg.VaultFetchToken),\n\t\t}, nil\n\tcase \"vault-pki\":\n\t\tsrc := NewVaultPKISource()\n\t\tsrc.CertPath = cfg.CertPath\n\t\tsrc.ClientCAPath = cfg.ClientCAPath\n\t\tsrc.CAUpgradeCN = cfg.CAUpgradeCN\n\t\tsrc.Refresh = cfg.Refresh\n\t\tsrc.Client = NewVaultClient(cfg.VaultFetchToken)\n\t\treturn src, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid certificate source %q\", cfg.Type)\n\t}\n}\n\n// TLSConfig creates a tls.Config which sets the GetCertificate field to a\n// certificate store which uses the given source to update the the certificates\n// on-demand.\n//\n// It also sets the ClientCAs field if src.LoadClientCAs returns a non-nil\n// value and sets ClientAuth to RequireAndVerifyClientCert.\nfunc TLSConfig(src Source, strictMatch bool, minVersion, maxVersion uint16, cipherSuites []uint16) (*tls.Config, error) {\n\tclientCAs, err := src.LoadClientCAs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsf := &singleflight.Group{}\n\tstore := NewStore()\n\tx := &tls.Config{\n\t\tMinVersion:   minVersion,\n\t\tMaxVersion:   maxVersion,\n\t\tCipherSuites: cipherSuites,\n\t\tNextProtos:   []string{\"h2\", \"http/1.1\"},\n\t\tGetCertificate: func(clientHello *tls.ClientHelloInfo) (cert *tls.Certificate, err error) {\n\t\t\tcert, err = getCertificate(store.certstore(), clientHello, strictMatch)\n\t\t\tif cert != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tswitch err {\n\t\t\tcase nil, ErrNoCertsStored:\n\t\t\t\t// Store doesn't contain a suitable cert. Perhaps the source can issue one?\n\t\t\tdefault:\n\t\t\t\t// an unrecoverable error\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tca, ok := src.(Issuer)\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tserverName := clientHello.ServerName\n\t\t\tx, err, _ := sf.Do(serverName, func() (interface{}, error) {\n\t\t\t\treturn ca.Issue(serverName)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn cert, err\n\t\t\t}\n\n\t\t\treturn x.(*tls.Certificate), nil\n\t\t},\n\t}\n\n\tif clientCAs != nil {\n\t\tx.ClientCAs = clientCAs\n\t\tx.ClientAuth = tls.RequireAndVerifyClientCert\n\t}\n\n\tgo func() {\n\t\tfor certs := range src.Certificates() {\n\t\t\tstore.SetCertificates(certs)\n\t\t}\n\t}()\n\n\treturn x, nil\n}\n"
  },
  {
    "path": "cert/source_test.go",
    "content": "package cert\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"golang.org/x/net/http2\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\tconsulapi \"github.com/hashicorp/consul/api\"\n\tvaultapi \"github.com/hashicorp/vault/api\"\n\t\"github.com/pascaldekloe/goe/verify\"\n)\n\nfunc TestTLSConfig(t *testing.T) {\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tcert, err := tls.X509KeyPair(certPEM, keyPEM)\n\tif err != nil {\n\t\tt.Fatalf(\"X509KeyPair: got %s want nil\", err)\n\t}\n\tpool := makeCertPool(certPEM)\n\tsrc := &StaticSource{cert, pool}\n\ttlsmin := uint16(0x1000)\n\ttlsmax := uint16(0x2000)\n\ttlsciphers := []uint16{0x1234, 0x5678}\n\tnextprotos := []string{\"h2\", \"http/1.1\"}\n\n\tcfg, err := TLSConfig(src, false, tlsmin, tlsmax, tlsciphers)\n\tif err != nil {\n\t\tt.Fatalf(\"got error %v want nil\", err)\n\t}\n\tif got, want := cfg.MinVersion, tlsmin; got != want {\n\t\tt.Fatalf(\"got tls min version %04x want %04x\", got, want)\n\t}\n\tif got, want := cfg.MaxVersion, tlsmax; got != want {\n\t\tt.Fatalf(\"got tls max version %04x want %04x\", got, want)\n\t}\n\tif got, want := cfg.CipherSuites, tlsciphers; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got tls ciphers %v want %v\", got, want)\n\t}\n\tif got, want := cfg.NextProtos, nextprotos; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got next protos %v want %v\", got, want)\n\t}\n\tif got, want := cfg.ClientCAs, pool; got != want {\n\t\tt.Fatalf(\"got client CAs %v want %v\", got, want)\n\t}\n\tif got, want := cfg.ClientAuth, tls.RequireAndVerifyClientCert; got != want {\n\t\tt.Fatalf(\"got client auth type %v want %v\", got, want)\n\t}\n\tif cfg.GetCertificate == nil {\n\t\tt.Fatalf(\"got GetCertificate() nil want not nil\")\n\t}\n}\n\nfunc TestNewSource(t *testing.T) {\n\tcertsource := func(typ string) config.CertSource {\n\t\treturn config.CertSource{\n\t\t\tType:         typ,\n\t\t\tName:         \"name\",\n\t\t\tCertPath:     \"cert\",\n\t\t\tKeyPath:      \"key\",\n\t\t\tClientCAPath: \"clientca\",\n\t\t\tCAUpgradeCN:  \"upgcn\",\n\t\t\tRefresh:      3 * time.Second,\n\t\t\tHeader:       http.Header{\"A\": []string{\"b\"}},\n\t\t}\n\t}\n\ttests := []struct {\n\t\tdesc string\n\t\tcfg  config.CertSource\n\t\tsrc  Source\n\t\terr  string\n\t}{\n\t\t{\n\t\t\tdesc: \"invalid\",\n\t\t\tcfg: config.CertSource{\n\t\t\t\tType: \"invalid\",\n\t\t\t},\n\t\t\tsrc: nil,\n\t\t\terr: `invalid certificate source \"invalid\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"file\",\n\t\t\tcfg:  certsource(\"file\"),\n\t\t\tsrc: FileSource{\n\t\t\t\tCertFile:       \"cert\",\n\t\t\t\tKeyFile:        \"key\",\n\t\t\t\tClientAuthFile: \"clientca\",\n\t\t\t\tCAUpgradeCN:    \"upgcn\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"path\",\n\t\t\tcfg:  certsource(\"path\"),\n\t\t\tsrc: PathSource{\n\t\t\t\tCertPath:     \"cert\",\n\t\t\t\tClientCAPath: \"clientca\",\n\t\t\t\tCAUpgradeCN:  \"upgcn\",\n\t\t\t\tRefresh:      3 * time.Second,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"http\",\n\t\t\tcfg:  certsource(\"http\"),\n\t\t\tsrc: HTTPSource{\n\t\t\t\tCertURL:     \"cert\",\n\t\t\t\tClientCAURL: \"clientca\",\n\t\t\t\tCAUpgradeCN: \"upgcn\",\n\t\t\t\tRefresh:     3 * time.Second,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"consul\",\n\t\t\tcfg:  certsource(\"consul\"),\n\t\t\tsrc: ConsulSource{\n\t\t\t\tCertURL:     \"cert\",\n\t\t\t\tClientCAURL: \"clientca\",\n\t\t\t\tCAUpgradeCN: \"upgcn\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"vault\",\n\t\t\tcfg:  certsource(\"vault\"),\n\t\t\tsrc: &VaultSource{\n\t\t\t\tClient:       DefaultVaultClient,\n\t\t\t\tCertPath:     \"cert\",\n\t\t\t\tClientCAPath: \"clientca\",\n\t\t\t\tCAUpgradeCN:  \"upgcn\",\n\t\t\t\tRefresh:      3 * time.Second,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tvar errmsg string\n\t\t\tsrc, err := NewSource(tt.cfg)\n\t\t\tif err != nil {\n\t\t\t\terrmsg = err.Error()\n\t\t\t}\n\t\t\tif got, want := errmsg, tt.err; got != want {\n\t\t\t\tt.Fatalf(\"%d: got %q want %q\", i, got, want)\n\t\t\t}\n\t\t\tgot, want := src, tt.src\n\t\t\tverify.Values(t, \"src\", got, want)\n\t\t})\n\t}\n}\n\ntype StaticSource struct {\n\tcert tls.Certificate\n\tpool *x509.CertPool\n}\n\nfunc (s StaticSource) Certificates() chan []tls.Certificate {\n\tch := make(chan []tls.Certificate, 1)\n\tch <- []tls.Certificate{s.cert}\n\tclose(ch)\n\treturn ch\n}\n\nfunc (s StaticSource) LoadClientCAs() (*x509.CertPool, error) {\n\treturn s.pool, nil\n}\n\nfunc TestStaticSource(t *testing.T) {\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tcert, err := tls.X509KeyPair(certPEM, keyPEM)\n\tif err != nil {\n\t\tt.Fatalf(\"X509KeyPair: got %s want nil\", err)\n\t}\n\ttestSource(t, StaticSource{cert, nil}, makeCertPool(certPEM), 0)\n}\n\nfunc TestFileSource(t *testing.T) {\n\tdir := t.TempDir()\n\tdefer os.RemoveAll(dir)\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tcertFile, keyFile := saveCert(dir, \"localhost\", certPEM, keyPEM)\n\ttestSource(t, FileSource{CertFile: certFile, KeyFile: keyFile}, makeCertPool(certPEM), 0)\n}\n\nfunc TestPathSource(t *testing.T) {\n\tdir := t.TempDir()\n\tdefer os.RemoveAll(dir)\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tsaveCert(dir, \"localhost\", certPEM, keyPEM)\n\ttestSource(t, PathSource{CertPath: dir}, makeCertPool(certPEM), 10*time.Millisecond)\n}\n\nfunc TestHTTPSource(t *testing.T) {\n\tdir := t.TempDir()\n\tdefer os.RemoveAll(dir)\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tcertFile, keyFile := saveCert(dir, \"localhost\", certPEM, keyPEM)\n\tlistFile := filepath.Base(certFile) + \"\\n\" + filepath.Base(keyFile) + \"\\n\"\n\twriteFile(filepath.Join(dir, \"list\"), []byte(listFile))\n\n\tsrv := httptest.NewServer(http.FileServer(http.Dir(dir)))\n\tdefer srv.Close()\n\n\ttestSource(t, HTTPSource{CertURL: srv.URL + \"/list\"}, makeCertPool(certPEM), 500*time.Millisecond)\n}\n\nfunc TestConsulSource(t *testing.T) {\n\tconst certURL = \"http://127.0.0.1:8500/v1/kv/fabio/test/consul-server\"\n\n\t// run a consul server if it isn't already running\n\t_, err := http.Get(\"http://127.0.0.1:8500/v1/status/leader\")\n\tif err != nil {\n\t\tconsul := os.Getenv(\"CONSUL_EXE\")\n\t\tif consul == \"\" {\n\t\t\tconsul = \"consul\"\n\t\t}\n\n\t\tversion, err := exec.Command(consul, \"--version\").Output()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to run %s --version\", consul)\n\t\t}\n\t\tcr := bytes.IndexRune(version, '\\n')\n\t\tt.Logf(\"Starting %s: %s\", consul, string(version[:cr]))\n\n\t\tstart := time.Now()\n\t\tcmd := exec.Command(consul, \"agent\", \"-bind\", \"127.0.0.1\", \"-server\", \"-dev\")\n\t\tif err := cmd.Start(); err != nil {\n\t\t\tt.Fatalf(\"Failed to start consul server. %s\", err)\n\t\t}\n\t\tdefer cmd.Process.Kill()\n\n\t\tisUp := func() bool {\n\t\t\tresp, err := http.Get(\"http://127.0.0.1:8500/v1/status/leader\")\n\t\t\t// /v1/status/leader returns '\\n\"\"' while consul is in leader election mode\n\t\t\t// and '\"127.0.0.1:8300\"' when not. So we punt by checking the\n\t\t\t// body length instead of the actual body content :)\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != 200 {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tn, err := io.Copy(io.Discard, resp.Body)\n\t\t\treturn err == nil && n > 10\n\t\t}\n\n\t\t// We need give consul ~8-10 seconds to become ready until I've\n\t\t// figured out whether we can speed this up. Make sure that this is\n\t\t// less than the global test timeout in Makefile.\n\t\tif !waitFor(12*time.Second, isUp) {\n\t\t\tt.Fatalf(\"Timeout waiting for consul server after %2.1f seconds\", time.Since(start).Seconds())\n\t\t}\n\t\tt.Logf(\"Consul is ready after %2.1f seconds\", time.Since(start).Seconds())\n\t} else {\n\t\tt.Log(\"Using existing consul server\")\n\t}\n\n\tconfig, key, err := parseConsulURL(certURL)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to parse consul url: %s\", err)\n\t}\n\n\tclient, err := consulapi.NewClient(config)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create consul client: %s\", err)\n\t}\n\tdefer func() { client.KV().DeleteTree(key, &consulapi.WriteOptions{}) }()\n\n\twrite := func(name string, value []byte) {\n\t\tp := &consulapi.KVPair{Key: key + \"/\" + name, Value: value}\n\t\t_, err := client.KV().Put(p, &consulapi.WriteOptions{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to write %q to consul: %s\", p.Key, err)\n\t\t}\n\t}\n\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\twrite(\"localhost-cert.pem\", certPEM)\n\twrite(\"localhost-key.pem\", keyPEM)\n\n\ttestSource(t, ConsulSource{CertURL: certURL}, makeCertPool(certPEM), 500*time.Millisecond)\n}\n\n// vaultServer starts a vault server in dev mode and waits until is ready.\nfunc vaultServer(t *testing.T, addr, rootToken string) (*exec.Cmd, *vaultapi.Client) {\n\tvault := os.Getenv(\"VAULT_EXE\")\n\tif vault == \"\" {\n\t\tvault = \"vault\"\n\t}\n\n\tversion, err := exec.Command(vault, \"--version\").Output()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to run %s --version\", vault)\n\t}\n\tt.Logf(\"Starting %s: %q\", vault, string(version))\n\n\tcmd := exec.Command(vault, \"server\", \"-dev\", \"-dev-root-token-id=\"+rootToken, \"-dev-listen-address=\"+addr)\n\tif err := cmd.Start(); err != nil {\n\t\tt.Fatalf(\"Failed to start vault server. %s\", err)\n\t}\n\n\tc, err := vaultapi.NewClient(&vaultapi.Config{Address: \"http://\" + addr})\n\tif err != nil {\n\t\tcmd.Process.Kill()\n\t\tt.Fatalf(\"NewClient failed: %s\", err)\n\t}\n\tc.SetToken(rootToken)\n\n\tisUp := func() bool {\n\t\tok, err := c.Sys().InitStatus()\n\t\treturn err == nil && ok\n\t}\n\tif !waitFor(time.Second, isUp) {\n\t\tcmd.Process.Kill()\n\t\tt.Fatal(\"Timeout waiting for vault server\")\n\t}\n\n\tpolicy := `\n\t# Vault < 0.7\n\tpath \"secret/fabio/cert\" {\n\t  capabilities = [\"list\"]\n\t}\n\t# Vault >= 0.7. Note the trailing slash.\n\tpath \"secret/fabio/cert/\" {\n\t  capabilities = [\"list\"]\n\t}\n\n\tpath \"secret/fabio/cert/*\" {\n\t  capabilities = [\"read\"]\n\t}\n\n\t# Vault >= 0.10. (KV Version 2)\n\tpath \"secret/metadata/fabio/cert/\" {\n\t  capabilities = [\"list\"]\n\t}\n\n\tpath \"secret/data/fabio/cert/*\" {\n\t  capabilities = [\"read\"]\n\t}\n\n\tpath \"test-pki/issue/fabio\" {\n\t  capabilities = [\"update\"]\n\t}\n\t`\n\n\tif err := c.Sys().PutPolicy(\"fabio\", policy); err != nil {\n\t\tcmd.Process.Kill()\n\t\tt.Fatalf(\"Could not create policy: %s\", err)\n\t}\n\n\treturn cmd, c\n}\n\nfunc makeToken(t *testing.T, c *vaultapi.Client, wrapTTL string, req *vaultapi.TokenCreateRequest) string {\n\tc.SetWrappingLookupFunc(func(string, string) string { return wrapTTL })\n\n\tresp, err := c.Auth().Token().Create(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Could not create a token: %s\", err)\n\t}\n\n\tif wrapTTL != \"\" {\n\t\tif resp.WrapInfo == nil || resp.WrapInfo.Token == \"\" {\n\t\t\tt.Fatalf(\"Could not create a wrapped token\")\n\t\t}\n\t\treturn resp.WrapInfo.Token\n\t}\n\n\tif resp.WrapInfo != nil && resp.WrapInfo.Token != \"\" {\n\t\tt.Fatalf(\"Got a wrapped token but was not expecting one\")\n\t}\n\n\treturn resp.Auth.ClientToken\n}\n\nvar vaultTestCases = []struct {\n\tdesc     string\n\twrapTTL  string\n\treq      *vaultapi.TokenCreateRequest\n\tdropWarn bool\n}{\n\t{\n\t\tdesc: \"renewable token\",\n\t\treq:  &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", Policies: []string{\"fabio\"}},\n\t},\n\t{\n\t\tdesc:     \"non-renewable token\",\n\t\treq:      &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", Renewable: new(bool), Policies: []string{\"fabio\"}},\n\t\tdropWarn: true,\n\t},\n\t{\n\t\tdesc: \"renewable orphan token\",\n\t\treq:  &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", NoParent: true, Policies: []string{\"fabio\"}},\n\t},\n\t{\n\t\tdesc:     \"non-renewable orphan token\",\n\t\treq:      &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", NoParent: true, Renewable: new(bool), Policies: []string{\"fabio\"}},\n\t\tdropWarn: true,\n\t},\n\t{\n\t\tdesc:    \"renewable wrapped token\",\n\t\twrapTTL: \"10s\",\n\t\treq:     &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", Policies: []string{\"fabio\"}},\n\t},\n\t{\n\t\tdesc:     \"non-renewable wrapped token\",\n\t\twrapTTL:  \"10s\",\n\t\treq:      &vaultapi.TokenCreateRequest{Lease: \"1m\", TTL: \"1m\", Renewable: new(bool), Policies: []string{\"fabio\"}},\n\t\tdropWarn: true,\n\t},\n}\n\nfunc TestVaultSource(t *testing.T) {\n\tconst (\n\t\taddr      = \"127.0.0.1:58421\"\n\t\trootToken = \"token\"\n\t\tcertPath  = \"secret/fabio/cert\"\n\t)\n\n\t// start a vault server\n\tvault, client := vaultServer(t, addr, rootToken)\n\tdefer vault.Process.Kill()\n\n\t// create a cert and store it in vault\n\tcertPEM, keyPEM := makePEM(\"localhost\", time.Minute)\n\tdata := map[string]interface{}{\"cert\": string(certPEM), \"key\": string(keyPEM)}\n\n\tvar nilSource *VaultSource // for calling helper methods\n\n\tmountPath, v2, err := nilSource.isKVv2(certPath, client)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tp := certPath + \"/localhost\"\n\tif v2 {\n\t\tt.Log(\"Vault: KV backend: V2\")\n\t\tdata = map[string]interface{}{\n\t\t\t\"data\":    data,\n\t\t\t\"options\": map[string]interface{}{},\n\t\t}\n\t\tp = nilSource.addPrefixToVKVPath(p, mountPath, \"data\")\n\t} else {\n\t\tt.Log(\"Vault: KV backend: V1\")\n\t}\n\tif _, err := client.Logical().Write(p, data); err != nil {\n\t\tt.Fatalf(\"logical.Write failed: %s\", err)\n\t}\n\n\tpool := makeCertPool(certPEM)\n\ttimeout := 500 * time.Millisecond\n\tfor _, tt := range vaultTestCases {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tsrc := &VaultSource{\n\t\t\t\tClient: &vaultClient{\n\t\t\t\t\taddr:  \"http://\" + addr,\n\t\t\t\t\ttoken: makeToken(t, client, tt.wrapTTL, tt.req),\n\t\t\t\t},\n\t\t\t\tCertPath: certPath,\n\t\t\t}\n\n\t\t\t// suppress the log warning about a non-renewable token\n\t\t\t// since this is the expected behavior.\n\t\t\tdropNotRenewableWarning = tt.dropWarn\n\t\t\ttestSource(t, src, pool, timeout)\n\t\t\tdropNotRenewableWarning = false\n\t\t})\n\t}\n}\n\nfunc TestVaultPKISource(t *testing.T) {\n\tconst (\n\t\taddr      = \"127.0.0.1:58421\"\n\t\trootToken = \"token\"\n\t\tcertPath  = \"test-pki/issue/fabio\"\n\t)\n\n\t// start a vault server\n\tvault, client := vaultServer(t, addr, rootToken)\n\tdefer vault.Process.Kill()\n\n\t// mount the PKI backend\n\terr := client.Sys().Mount(\"test-pki\", &vaultapi.MountInput{\n\t\tType: \"pki\",\n\t\tConfig: vaultapi.MountConfigInput{\n\t\t\tDefaultLeaseTTL: \"1h\", // default validity period of issued certificates\n\t\t\tMaxLeaseTTL:     \"2h\", // maximum validity period of issued certificates\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Mount pki backend failed: %s\", err)\n\t}\n\n\t// generate root CA cert\n\tresp, err := client.Logical().Write(\"test-pki/root/generate/internal\", map[string]interface{}{\n\t\t\"common_name\": \"fabio-ca.com\",\n\t\t\"ttl\":         \"2h\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Generate root failed: %s\", err)\n\t}\n\tcaPool := makeCertPool([]byte(resp.Data[\"certificate\"].(string)))\n\n\t// create role\n\trole := filepath.Base(certPath)\n\t_, err = client.Logical().Write(\"test-pki/roles/\"+role, map[string]interface{}{\n\t\t\"allowed_domains\": \"\",\n\t\t\"allow_localhost\": true,\n\t\t\"allow_ip_sans\":   true,\n\t\t\"organization\":    \"Fabio Test\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Write role failed: %s\", err)\n\t}\n\n\tfor _, tt := range vaultTestCases {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tsrc := NewVaultPKISource()\n\t\t\tsrc.Client = &vaultClient{\n\t\t\t\taddr:  \"http://\" + addr,\n\t\t\t\ttoken: makeToken(t, client, tt.wrapTTL, tt.req),\n\t\t\t}\n\t\t\tsrc.CertPath = certPath\n\n\t\t\t// suppress the log warning about a non-renewable token\n\t\t\t// since this is the expected behavior.\n\t\t\tdropNotRenewableWarning = tt.dropWarn\n\t\t\ttestSource(t, src, caPool, 0)\n\t\t\tdropNotRenewableWarning = false\n\t\t})\n\t}\n}\n\n// testSource runs an integration test by making an HTTPS request\n// to https://localhost/ expecting that the source provides a valid\n// certificate for \"localhost\". rootCAs is expected to contain a\n// valid root certificate or the server certificate itself so that\n// the HTTPS client can validate the certificate presented by the\n// server.\nfunc testSource(t *testing.T, source Source, rootCAs *x509.CertPool, sleep time.Duration) {\n\tconst NoStrictMatch = false\n\tsrvConfig, err := TLSConfig(source, NoStrictMatch, 0, 0, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"TLSConfig: got %q want nil\", err)\n\t}\n\n\t// give the source some time to initialize if necessary\n\ttime.Sleep(sleep)\n\n\t// create an http client that will accept the root CAs\n\t// otherwise the HTTPS client will not verify the\n\t// certificate presented by the server.\n\thttp11 := http11Client(rootCAs)\n\thttp20, err := http20Client(rootCAs)\n\tif err != nil {\n\t\tt.Fatal(\"http20Client: \", err)\n\t}\n\n\t// disable log output for the next call to prevent\n\t// confusing log messages since they are expected\n\t// http: TLS handshake error from 127.0.0.1:55044: remote error: bad certificate\n\tlog.SetOutput(io.Discard)\n\tdefer log.SetOutput(os.Stderr)\n\n\t// fail calls https://localhost.org/ for which certificate validation\n\t// should fail since the hostname differs from the one in the certificate.\n\tfail := func(client *http.Client) {\n\t\t_, _, err := roundtrip(\"localhost.org\", srvConfig, client)\n\t\tgot, want := err, \"x509: certificate is valid for localhost, not localhost.org\"\n\t\tif got == nil || !strings.Contains(got.Error(), want) {\n\t\t\tt.Fatalf(\"got %q want %q\", got, want)\n\t\t}\n\t}\n\n\t// succeed executes a roundtrip to https://localhost/ which\n\t// should return 200 OK and wantBody.\n\tsucceed := func(client *http.Client, wantBody string) {\n\t\tcode, body, err := roundtrip(\"localhost\", srvConfig, client)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"got %v want nil\", err)\n\t\t}\n\t\tif got, want := code, 200; got != want {\n\t\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t\t}\n\t\tif got, want := body, wantBody; got != want {\n\t\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t\t}\n\t}\n\n\t// make a call for which certificate validation succeeds.\n\tsucceed(http11, \"OK HTTP/1.1\")\n\tsucceed(http20, \"OK HTTP/2.0\")\n\n\t// now make the call that should fail.\n\tfail(http11)\n\tfail(http20)\n}\n\n// roundtrip starts a TLS server with the given server configuration and\n// then sends an SNI request with the given serverName.\nfunc roundtrip(serverName string, srvConfig *tls.Config, client *http.Client) (code int, body string, err error) {\n\t// create an HTTPS server and start it. It will be listening on 127.0.0.1\n\tsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, \"OK \", r.Proto)\n\t}))\n\tsrv.TLS = srvConfig\n\tsrv.StartTLS()\n\tdefer srv.Close()\n\n\t// configure SNI\n\tclient.Transport.(*http.Transport).TLSClientConfig.ServerName = serverName\n\n\t// give the tls server some time to start up\n\ttime.Sleep(10 * time.Millisecond)\n\n\tresp, err := client.Get(srv.URL)\n\tif err != nil {\n\t\treturn 0, \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn 0, \"\", err\n\t}\n\treturn resp.StatusCode, string(data), nil\n}\n\n// http11Client returns an HTTP client which can only\n// execute HTTP/1.1 requests via TLS.\nfunc http11Client(rootCAs *x509.CertPool) *http.Client {\n\tt := &http.Transport{\n\t\tTLSClientConfig: &tls.Config{\n\t\t\tRootCAs: rootCAs,\n\t\t},\n\t}\n\treturn &http.Client{Transport: t}\n}\n\n// http20Client returns an HTTP client which can\n// execute HTTP/2.0 requests via TLS if the server\n// supports it.\nfunc http20Client(rootCAs *x509.CertPool) (*http.Client, error) {\n\tt := &http.Transport{\n\t\tTLSClientConfig: &tls.Config{\n\t\t\tRootCAs: rootCAs,\n\t\t},\n\t}\n\tif err := http2.ConfigureTransport(t); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &http.Client{Transport: t}, nil\n}\n\nfunc writeFile(filename string, data []byte) {\n\tif err := os.WriteFile(filename, data, 0644); err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc makeCertPool(x ...[]byte) *x509.CertPool {\n\tp := x509.NewCertPool()\n\tfor _, b := range x {\n\t\t// https://github.com/fabiolb/fabio/issues/434\n\t\tif ok := p.AppendCertsFromPEM(b); !ok {\n\t\t\tpanic(\"failed to add cert from PEM. Is the CN a DNS compatible name?\")\n\t\t}\n\t}\n\treturn p\n}\n\nfunc saveCert(dir, host string, certPEM, keyPEM []byte) (certFile, keyFile string) {\n\tcertFile, keyFile = filepath.Join(dir, host+\"-cert.pem\"), filepath.Join(dir, host+\"-key.pem\")\n\twriteFile(certFile, certPEM)\n\twriteFile(keyFile, keyPEM)\n\treturn certFile, keyFile\n}\n\n// makePEM creates a self-signed RSA certificate as two PEM blocks.\n// taken from crypto/tls/generate_cert.go\nfunc makePEM(host string, validFor time.Duration) (certPEM, keyPEM []byte) {\n\tconst bits = 1024\n\tpriv, err := rsa.GenerateKey(rand.Reader, bits)\n\tif err != nil {\n\t\tpanic(\"Failed to generate private key: \" + err.Error())\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: big.NewInt(1),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Fabio Co\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().Add(validFor),\n\t\tIsCA:                  true,\n\t\tDNSNames:              []string{host},\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t}\n\n\tderBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)\n\tif err != nil {\n\t\tpanic(\"Failed to create certificate: \" + err.Error())\n\t}\n\n\tvar cert, key bytes.Buffer\n\tpem.Encode(&cert, &pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes})\n\tpem.Encode(&key, &pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(priv)})\n\treturn cert.Bytes(), key.Bytes()\n}\n\nfunc makeCert(host string, validFor time.Duration) tls.Certificate {\n\tcertPEM, keyPEM := makePEM(host, validFor)\n\tcert, err := tls.X509KeyPair(certPEM, keyPEM)\n\tif err != nil {\n\t\tpanic(\"Failed to create certificate: \" + err.Error())\n\t}\n\treturn cert\n}\n\nfunc waitFor(timeout time.Duration, up func() bool) bool {\n\tuntil := time.Now().Add(timeout)\n\tfor {\n\t\tif time.Now().After(until) {\n\t\t\treturn false\n\t\t}\n\t\tif up() {\n\t\t\treturn true\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n}\n"
  },
  {
    "path": "cert/store.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"log\"\n\t\"strings\"\n\t\"sync/atomic\"\n)\n\n// Store provides a dynamic certificate store which can be updated at\n// runtime and is safe for concurrent use.\ntype Store struct {\n\tcs atomic.Value\n}\n\n// NewStore creates an empty certificate store.\nfunc NewStore() *Store {\n\ts := new(Store)\n\ts.cs.Store(certstore{})\n\treturn s\n}\n\n// SetCertificates replaces the certificates of the store.\nfunc (s *Store) SetCertificates(certs []tls.Certificate) {\n\tcs := certstore{Certificates: certs}\n\tcs.BuildNameToCertificate()\n\ts.cs.Store(cs)\n\tvar names []string\n\tfor name := range cs.NameToCertificate {\n\t\tnames = append(names, name)\n\t}\n\tlog.Printf(\"[INFO] cert: Store has certificates for [%q]\", strings.Join(names, \",\"))\n}\n\nfunc (s *Store) certstore() certstore {\n\treturn s.cs.Load().(certstore)\n}\n\nvar ErrNoCertsStored = errors.New(\"cert: no certificates stored\")\n\nfunc getCertificate(cs certstore, clientHello *tls.ClientHelloInfo, strictMatch bool) (cert *tls.Certificate, err error) {\n\tif len(cs.Certificates) == 0 {\n\t\treturn nil, ErrNoCertsStored\n\t}\n\n\t// There's only one choice, so no point doing any work.\n\t// However, if fallback is disabled we need to check.\n\tif !strictMatch && (len(cs.Certificates) == 1 || cs.NameToCertificate == nil) {\n\t\treturn &cs.Certificates[0], nil\n\t}\n\n\tname := strings.ToLower(clientHello.ServerName)\n\tfor len(name) > 0 && name[len(name)-1] == '.' {\n\t\tname = name[:len(name)-1]\n\t}\n\n\tif cert, ok := cs.NameToCertificate[name]; ok {\n\t\treturn cert, nil\n\t}\n\n\t// try replacing labels in the name with wildcards until we get a match\n\tlabels := strings.Split(name, \".\")\n\tfor i := range labels {\n\t\tlabels[i] = \"*\"\n\t\tcandidate := strings.Join(labels, \".\")\n\t\tif cert, ok := cs.NameToCertificate[candidate]; ok {\n\t\t\treturn cert, nil\n\t\t}\n\t}\n\n\t// If nothing matches, return the first certificate\n\t// unless fallback to the first cert is disabled.\n\tif strictMatch {\n\t\treturn nil, nil\n\t}\n\treturn &cs.Certificates[0], nil\n}\n\ntype certstore struct {\n\tNameToCertificate map[string]*tls.Certificate\n\tCertificates      []tls.Certificate\n}\n\n// BuildNameToCertificate parses Certificates and builds NameToCertificate\n// from the CommonName and SubjectAlternateName fields of each of the leaf\n// certificates.\nfunc (c *certstore) BuildNameToCertificate() {\n\tc.NameToCertificate = make(map[string]*tls.Certificate)\n\tfor i := range c.Certificates {\n\t\tcert := &c.Certificates[i]\n\t\tx509Cert, err := x509.ParseCertificate(cert.Certificate[0])\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(x509Cert.Subject.CommonName) > 0 {\n\t\t\tc.NameToCertificate[x509Cert.Subject.CommonName] = cert\n\t\t}\n\t\tfor _, san := range x509Cert.DNSNames {\n\t\t\tc.NameToCertificate[san] = cert\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cert/store_test.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestGetCertificate(t *testing.T) {\n\tfooCert := makeCert(\"foo.com\", time.Minute)\n\tbarCert := makeCert(\"bar.com\", time.Minute)\n\twildBarCert := makeCert(\"*.bar.com\", time.Minute)\n\n\ttests := []struct {\n\t\tdesc   string\n\t\tcerts  []tls.Certificate\n\t\thello  *tls.ClientHelloInfo\n\t\tstrict bool\n\t\tcert   *tls.Certificate\n\t\terr    error\n\t}{\n\t\t// edge cases\n\t\t{\n\t\t\tdesc:  \"no certs\",\n\t\t\tcerts: nil,\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"foo.com\"},\n\t\t\tcert:  nil,\n\t\t\terr:   errors.New(\"cert: no certificates stored\"),\n\t\t},\n\t\t{\n\t\t\tdesc:  \"server name ends in dot\",\n\t\t\tcerts: []tls.Certificate{fooCert, barCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"bar.com.\"},\n\t\t\tcert:  &barCert,\n\t\t\terr:   nil,\n\t\t},\n\n\t\t// happy flows\n\t\t{\n\t\t\tdesc:  \"one cert exact match\",\n\t\t\tcerts: []tls.Certificate{fooCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"foo.com\"},\n\t\t\tcert:  &fooCert,\n\t\t\terr:   nil,\n\t\t},\n\t\t{\n\t\t\tdesc:  \"one cert fallback\",\n\t\t\tcerts: []tls.Certificate{fooCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"bar.com\"},\n\t\t\tcert:  &fooCert,\n\t\t\terr:   nil,\n\t\t},\n\t\t{\n\t\t\tdesc:   \"one cert strict match\",\n\t\t\tcerts:  []tls.Certificate{fooCert},\n\t\t\thello:  &tls.ClientHelloInfo{ServerName: \"bar.com\"},\n\t\t\tcert:   nil,\n\t\t\tstrict: true,\n\t\t\terr:    nil,\n\t\t},\n\t\t{\n\t\t\tdesc:  \"two certs exact match\",\n\t\t\tcerts: []tls.Certificate{fooCert, barCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"bar.com\"},\n\t\t\tcert:  &barCert,\n\t\t\terr:   nil,\n\t\t},\n\t\t{\n\t\t\tdesc:  \"two certs fallback\",\n\t\t\tcerts: []tls.Certificate{fooCert, barCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"whiz.com\"},\n\t\t\tcert:  &fooCert,\n\t\t\terr:   nil,\n\t\t},\n\t\t{\n\t\t\tdesc:   \"two certs strict match\",\n\t\t\tcerts:  []tls.Certificate{fooCert, barCert},\n\t\t\thello:  &tls.ClientHelloInfo{ServerName: \"whiz.com\"},\n\t\t\tcert:   nil,\n\t\t\tstrict: true,\n\t\t\terr:    nil,\n\t\t},\n\t\t{\n\t\t\tdesc:  \"wildcard cert\",\n\t\t\tcerts: []tls.Certificate{fooCert, wildBarCert},\n\t\t\thello: &tls.ClientHelloInfo{ServerName: \"quux.bar.com\"},\n\t\t\tcert:  &wildBarCert,\n\t\t\terr:   nil,\n\t\t},\n\t\t{\n\t\t\tdesc:   \"wildcard cert strict match\",\n\t\t\tcerts:  []tls.Certificate{fooCert, wildBarCert},\n\t\t\thello:  &tls.ClientHelloInfo{ServerName: \"quux.bar.com\"},\n\t\t\tcert:   &wildBarCert,\n\t\t\tstrict: true,\n\t\t\terr:    nil,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\tcs := certstore{Certificates: tt.certs}\n\t\tcs.BuildNameToCertificate()\n\t\tcert, err := getCertificate(cs, tt.hello, tt.strict)\n\t\tif got, want := err, tt.err; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"%d: %q: got %v want %v\", i, tt.desc, got, want)\n\t\t\tcontinue\n\t\t}\n\t\tif got, want := cert, tt.cert; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"%d: %q: got %+v want %+v\", i, tt.desc, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cert/vault_client.go",
    "content": "package cert\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/vault/api\"\n\t\"github.com/hashicorp/vault/sdk/helper/consts\"\n)\n\n// vaultClient wraps an *api.Client and takes care of token renewal\n// automatically.\ntype vaultClient struct {\n\tclient           *api.Client\n\taddr             string // overrides the default config\n\ttoken            string // overrides the VAULT_TOKEN environment variable\n\tfetchVaultToken  string\n\tprevFetchedToken string\n\n\tmu sync.Mutex\n}\n\nfunc NewVaultClient(fetchVaultToken string) *vaultClient {\n\treturn &vaultClient{\n\t\tfetchVaultToken: fetchVaultToken,\n\t}\n}\n\nvar DefaultVaultClient = &vaultClient{}\n\nfunc (c *vaultClient) Get() (*api.Client, error) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.client != nil {\n\t\tif c.fetchVaultToken != \"\" {\n\t\t\ttoken := strings.TrimSpace(getVaultToken(c.fetchVaultToken))\n\t\t\tif token != c.prevFetchedToken {\n\t\t\t\tlog.Printf(\"[DEBUG] vault: token has changed, setting new token\")\n\t\t\t\t// did we get a wrapped token?\n\t\t\t\tresp, err := c.client.Logical().Unwrap(token)\n\t\t\t\tswitch {\n\t\t\t\tcase err == nil:\n\t\t\t\t\tlog.Printf(\"[INFO] vault: Unwrapped token %s\", token)\n\t\t\t\t\tc.client.SetToken(resp.Auth.ClientToken)\n\t\t\t\tcase strings.HasPrefix(err.Error(), \"no value found at\"):\n\t\t\t\t\t// not a wrapped token\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tc.prevFetchedToken = token\n\t\t\t}\n\t\t}\n\t\treturn c.client, nil\n\t}\n\n\tconf := api.DefaultConfig()\n\tif err := conf.ReadEnvironment(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.addr != \"\" {\n\t\tconf.Address = c.addr\n\t}\n\tclient, err := api.NewClient(conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.fetchVaultToken != \"\" {\n\t\ttoken := strings.TrimSpace(getVaultToken(c.fetchVaultToken))\n\t\tlog.Printf(\"[DEBUG] vault: fetching initial token\")\n\t\tif token != c.prevFetchedToken {\n\t\t\tc.token = token\n\t\t\tc.prevFetchedToken = token\n\t\t}\n\t}\n\tif c.token != \"\" {\n\t\tclient.SetToken(c.token)\n\t}\n\ttoken := client.Token()\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"vault: no token\")\n\t}\n\n\t// did we get a wrapped token?\n\tresp, err := client.Logical().Unwrap(token)\n\tvar respErr *api.ResponseError\n\tcontains := func(haystack []string, needle string) bool {\n\t\tfor _, h := range haystack {\n\t\t\tif h == needle {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\tswitch {\n\tcase err == nil:\n\t\tlog.Printf(\"[INFO] vault: Unwrapped token %s\", token)\n\t\tclient.SetToken(resp.Auth.ClientToken)\n\tcase errors.As(err, &respErr) &&\n\t\tcontains(respErr.Errors, consts.ErrInvalidWrappingToken.Error()):\n\t\t// not wrapped\n\tdefault:\n\t\treturn nil, err\n\t}\n\n\tc.client = client\n\tgo c.keepTokenAlive()\n\n\treturn client, nil\n}\n\n// dropNotRenewableWarning controls whether the 'Token is not renewable'\n// warning is logged. This is useful for testing where this is the expected\n// behavior. On production, this should always be set to false.\nvar dropNotRenewableWarning bool\n\nfunc (c *vaultClient) keepTokenAlive() {\n\tresp, err := c.client.Auth().Token().LookupSelf()\n\tif err != nil {\n\t\tlog.Printf(\"[WARN] vault: lookup-self failed, token renewal is disabled: %s\", err)\n\t\treturn\n\t}\n\n\tb, _ := json.Marshal(resp.Data)\n\tvar data struct {\n\t\tExpireTime  time.Time `json:\"expire_time\"`\n\t\tTTL         int       `json:\"ttl\"`\n\t\tCreationTTL int       `json:\"creation_ttl\"`\n\t\tRenewable   bool      `json:\"renewable\"`\n\t}\n\tif err := json.Unmarshal(b, &data); err != nil {\n\t\tlog.Printf(\"[WARN] vault: lookup-self failed, token renewal is disabled: %s\", err)\n\t\treturn\n\t}\n\n\tswitch {\n\tcase data.Renewable:\n\t\t// no-op\n\tcase data.ExpireTime.IsZero():\n\t\t// token doesn't expire\n\t\treturn\n\tcase dropNotRenewableWarning:\n\t\treturn\n\tdefault:\n\t\tttl := time.Until(data.ExpireTime)\n\t\tttl = ttl.Round(time.Second)\n\t\tlog.Printf(\"[WARN] vault: Token is not renewable and will expire %s from now at %s\",\n\t\t\tttl, data.ExpireTime.Format(time.RFC3339))\n\t\treturn\n\t}\n\n\tttl := time.Duration(data.TTL) * time.Second\n\ttimer := time.NewTimer(ttl / 2)\n\n\tfor range timer.C {\n\t\tresp, err := c.client.Auth().Token().RenewSelf(data.CreationTTL)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] vault: Failed to renew token: %s\", err)\n\t\t\ttimer.Reset(time.Second) // TODO: backoff? abort after N consecutive failures?\n\t\t\tcontinue\n\t\t}\n\n\t\tif !resp.Auth.Renewable || resp.Auth.LeaseDuration == 0 {\n\t\t\t// token isn't renewable anymore, we're done.\n\t\t\treturn\n\t\t}\n\n\t\tttl = time.Duration(resp.Auth.LeaseDuration) * time.Second\n\t\ttimer.Reset(ttl / 2)\n\t}\n}\n\nfunc getVaultToken(c string) string {\n\tvar token string\n\tc = strings.TrimSpace(c)\n\tcArray := strings.SplitN(c, \":\", 2)\n\tif len(cArray) < 2 {\n\t\tlog.Printf(\"[WARN] vault: vaultfetchtoken not properly set\")\n\t\treturn token\n\t}\n\tswitch cArray[0] {\n\tcase \"file\":\n\t\tb, err := os.ReadFile(cArray[1]) // just pass the file name\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] vault: Failed to fetch token from  %s\", c)\n\t\t} else {\n\t\t\ttoken = string(b)\n\t\t\tlog.Printf(\"[DEBUG] vault: Successfully fetched token from %s\", c)\n\t\t\treturn token\n\t\t}\n\tcase \"env\":\n\t\ttoken = os.Getenv(cArray[1])\n\t\tif len(token) == 0 {\n\t\t\tlog.Printf(\"[WARN] vault: Failed to fetch token from  %s\", c)\n\t\t} else {\n\t\t\tlog.Printf(\"[DEBUG] vault: Successfully fetched token from %s\", c)\n\t\t\treturn token\n\t\t}\n\tdefault:\n\t\tlog.Printf(\"[WARN] vault: vaultfetchtoken not properly set\")\n\t}\n\treturn token\n}\n"
  },
  {
    "path": "cert/vault_pki_source.go",
    "content": "package cert\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/big\"\n\t\"sync\"\n\t\"time\"\n)\n\n// VaultPKISource implements a certificate source which issues TLS certificates\n// on-demand using a Vault PKI backend. Client authorization certificates are\n// loaded from a generic backend (same as in VaultSource). The Vault token\n// should be set through the VAULT_TOKEN environment variable.\n//\n// The TLS certificates are re-issued automatically before they expire.\ntype VaultPKISource struct {\n\tClient *vaultClient\n\n\tcertsCh chan []tls.Certificate\n\n\tcerts        map[string]tls.Certificate // issued certs\n\tCertPath     string\n\tClientCAPath string\n\tCAUpgradeCN  string\n\n\t// Re-issue certificates this long before they expire. Cannot be less then\n\t// one hour.\n\tRefresh time.Duration\n\n\tmu sync.Mutex\n}\n\nfunc NewVaultPKISource() *VaultPKISource {\n\treturn &VaultPKISource{\n\t\tcerts:   make(map[string]tls.Certificate, 0),\n\t\tcertsCh: make(chan []tls.Certificate, 1),\n\t}\n}\n\nfunc (s *VaultPKISource) LoadClientCAs() (*x509.CertPool, error) {\n\treturn (&VaultSource{\n\t\tClient:       s.Client,\n\t\tClientCAPath: s.ClientCAPath,\n\t\tCAUpgradeCN:  s.CAUpgradeCN,\n\t}).LoadClientCAs()\n}\n\nfunc (s *VaultPKISource) Certificates() chan []tls.Certificate {\n\treturn s.certsCh\n}\n\nfunc (s *VaultPKISource) Issue(commonName string) (*tls.Certificate, error) {\n\tc, err := s.Client.Get()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: client: %s\", err)\n\t}\n\n\tresp, err := c.Logical().Write(s.CertPath, map[string]interface{}{\n\t\t\"common_name\": commonName,\n\t})\n\tif err != nil {\n\t\tfmt.Printf(\"Issue: %v\\n\", err)\n\t\treturn nil, fmt.Errorf(\"vault: issue: %s\", err)\n\t}\n\n\tb, _ := json.Marshal(resp.Data)\n\tvar data struct {\n\t\tPrivateKey  string   `json:\"private_key\"`\n\t\tCertificate string   `json:\"certificate\"`\n\t\tCAChain     []string `json:\"ca_chain\"`\n\t}\n\tif err := json.Unmarshal(b, &data); err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: issue: %s\", err)\n\t}\n\n\tif data.PrivateKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"vault: issue: missing private key\")\n\t}\n\tif data.Certificate == \"\" {\n\t\treturn nil, fmt.Errorf(\"vault: issue: missing certificate\")\n\t}\n\n\tkey := []byte(data.PrivateKey)\n\tfullChain := []byte(data.Certificate)\n\tfor _, c := range data.CAChain {\n\t\tfullChain = append(fullChain, '\\n')\n\t\tfullChain = append(fullChain, []byte(c)...)\n\t}\n\n\tcert, err := tls.X509KeyPair(fullChain, key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: issue: %s\", err)\n\t}\n\n\tx509Cert, err := x509.ParseCertificate(cert.Certificate[0])\n\tif err != nil {\n\t\t// Should never happen because x509.ParseCertificate did this\n\t\t// successfully already, but threw the result away.\n\t\treturn nil, fmt.Errorf(\"vault: issue: %s\", err)\n\t}\n\n\trefresh := s.Refresh\n\tif refresh < time.Hour {\n\t\trefresh = time.Hour\n\t}\n\n\texpires := x509Cert.NotAfter\n\tcertTTL := time.Until(expires) - refresh\n\ttime.AfterFunc(certTTL, func() {\n\t\t_, err := s.Issue(commonName)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] cert: vault: Failed to re-issue cert for %s: %s\", commonName, err)\n\t\t\t// TODO: Now what? Retry? Do nothing?\n\t\t\treturn\n\t\t}\n\t})\n\n\ts.mu.Lock()\n\ts.certs[commonName] = cert\n\tallCerts := make([]tls.Certificate, 0, len(s.certs))\n\tfor _, c := range s.certs {\n\t\tallCerts = append(allCerts, c)\n\t}\n\ts.mu.Unlock()\n\n\tgo func() { s.certsCh <- allCerts }()\n\tlog.Printf(\"[INFO] cert: vault: issued cert for %s; serial = %s\", commonName, s.formatSerial(x509Cert.SerialNumber))\n\n\treturn &cert, nil\n}\n\nfunc (*VaultPKISource) formatSerial(sn *big.Int) string {\n\tvar buf bytes.Buffer\n\tfor _, b := range sn.Bytes() {\n\t\tif buf.Len() > 0 {\n\t\t\tbuf.WriteByte('-')\n\t\t}\n\t\tfmt.Fprintf(&buf, \"%02x\", b)\n\t}\n\treturn buf.String()\n}\n"
  },
  {
    "path": "cert/vault_source.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"log\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/vault/api\"\n)\n\n// VaultSource implements a certificate source which loads\n// TLS and client authorization certificates from a Vault server.\n// The Vault token should be set through the VAULT_TOKEN environment\n// variable.\n//\n// The TLS certificates are updated automatically when Refresh\n// is not zero. Refresh cannot be less than one second to prevent\n// busy loops.\ntype VaultSource struct {\n\tClient       *vaultClient\n\tCertPath     string\n\tClientCAPath string\n\tCAUpgradeCN  string\n\tRefresh      time.Duration\n}\n\nfunc (s *VaultSource) LoadClientCAs() (*x509.CertPool, error) {\n\tif s.ClientCAPath == \"\" {\n\t\treturn nil, nil\n\t}\n\treturn newCertPool(s.ClientCAPath, s.CAUpgradeCN, s.load)\n}\n\nfunc (s *VaultSource) Certificates() chan []tls.Certificate {\n\tch := make(chan []tls.Certificate, 1)\n\tgo watch(ch, s.Refresh, s.CertPath, s.load)\n\treturn ch\n}\n\nfunc (s *VaultSource) load(path string) (pemBlocks map[string][]byte, err error) {\n\tpemBlocks = map[string][]byte{}\n\n\t// get will read a key=value pair from the secret\n\t// and store it as <name>-{cert,key}.pem so that\n\t// they are recognized by the post-processing function\n\t// which assembles the certificates.\n\t// The value can be stored either as string or []byte.\n\tget := func(name, typ string, secret *api.Secret, v2 bool) {\n\t\tdata := secret.Data\n\t\tif v2 {\n\t\t\tx, ok := secret.Data[\"data\"]\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdata, ok = x.(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tv := data[typ]\n\t\tif v == nil {\n\t\t\treturn\n\t\t}\n\n\t\tvar b []byte\n\t\tswitch v := v.(type) {\n\t\tcase string:\n\t\t\tb = []byte(v)\n\t\tcase []byte:\n\t\t\tb = v\n\t\tdefault:\n\t\t\tlog.Printf(\"[WARN] cert: key %s has type %T\", name, v)\n\t\t\treturn\n\t\t}\n\n\t\tpemBlocks[name+\"-\"+typ+\".pem\"] = b\n\t}\n\n\tc, err := s.Client.Get()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: client: %s\", err)\n\t}\n\n\tmountPath, v2, err := s.isKVv2(path, c)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: query mount path: %s\", err)\n\t}\n\n\t// get the subkeys under 'path'.\n\t// Each subkey refers to a certificate.\n\tp := path\n\tif v2 {\n\t\tp = s.addPrefixToVKVPath(p, mountPath, \"metadata\")\n\t}\n\n\tcerts, err := c.Logical().List(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vault: list: %s\", err)\n\t}\n\tif certs == nil || certs.Data[\"keys\"] == nil {\n\t\treturn nil, nil\n\t}\n\n\tfor _, x := range certs.Data[\"keys\"].([]interface{}) {\n\t\tname := x.(string)\n\t\tp := path + \"/\" + name\n\t\tif v2 {\n\t\t\tp = s.addPrefixToVKVPath(p, mountPath, \"data\")\n\t\t}\n\t\tsecret, err := c.Logical().Read(p)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] cert: Failed to read %s from Vault: %s\", p, err)\n\t\t\tcontinue\n\t\t}\n\t\tif secret == nil {\n\t\t\tlog.Printf(\"[WARN] cert: Failed to find %s in Vault: %s\", p, err)\n\t\t\tcontinue\n\t\t}\n\t\tget(name, \"cert\", secret, v2)\n\t\tget(name, \"key\", secret, v2)\n\t}\n\n\treturn pemBlocks, nil\n}\n\nfunc (s *VaultSource) addPrefixToVKVPath(p, mountPath, apiPrefix string) string {\n\tp = strings.TrimPrefix(p, mountPath)\n\treturn path.Join(mountPath, apiPrefix, p)\n}\n\nfunc (s *VaultSource) isKVv2(path string, client *api.Client) (string, bool, error) {\n\tmountPath, version, err := s.kvPreflightVersionRequest(client, path)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\n\treturn mountPath, version == 2, nil\n}\n\nfunc (s *VaultSource) kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) {\n\tresp, err := client.Logical().ReadRaw(\"sys/internal/ui/mounts/\" + path)\n\tif resp != nil {\n\t\tdefer resp.Body.Close()\n\t}\n\tif err != nil {\n\t\t// If we get a 404 we are using an older version of vault, default to\n\t\t// version 1\n\t\tif resp != nil && resp.StatusCode == 404 {\n\t\t\treturn \"\", 1, nil\n\t\t}\n\n\t\treturn \"\", 0, err\n\t}\n\n\tsecret, err := api.ParseSecret(resp.Body)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\tvar mountPath string\n\tif mountPathRaw, ok := secret.Data[\"path\"]; ok {\n\t\tmountPath = mountPathRaw.(string)\n\t}\n\toptions := secret.Data[\"options\"]\n\tif options == nil {\n\t\treturn mountPath, 1, nil\n\t}\n\tversionRaw := options.(map[string]interface{})[\"version\"]\n\tif versionRaw == nil {\n\t\treturn mountPath, 1, nil\n\t}\n\tversion := versionRaw.(string)\n\tswitch version {\n\tcase \"\", \"1\":\n\t\treturn mountPath, 1, nil\n\tcase \"2\":\n\t\treturn mountPath, 2, nil\n\t}\n\n\treturn mountPath, 1, nil\n}\n"
  },
  {
    "path": "cert/watch.go",
    "content": "package cert\n\nimport (\n\t\"crypto/tls\"\n\t\"log\"\n\t\"reflect\"\n\t\"time\"\n)\n\n// watch monitors the result of the loadFn function for changes.\nfunc watch(ch chan []tls.Certificate, refresh time.Duration, path string, loadFn func(path string) (map[string][]byte, error)) {\n\tonce := refresh <= 0\n\n\t// do not refresh more often than once a second to prevent busy loops\n\tif refresh < time.Second {\n\t\trefresh = time.Second\n\t}\n\n\tvar last map[string][]byte\n\tfor {\n\t\tnext, err := loadFn(path)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] cert: Cannot load certificates from %s. %s\", path, err)\n\t\t\ttime.Sleep(refresh)\n\t\t\tcontinue\n\t\t}\n\n\t\tif reflect.DeepEqual(next, last) {\n\t\t\ttime.Sleep(refresh)\n\t\t\tcontinue\n\t\t}\n\n\t\tcerts, err := loadCertificates(next)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] cert: Cannot make certificates: %s\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tch <- certs\n\t\tlast = next\n\n\t\tif once {\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\t\"time\"\n)\n\ntype Config struct {\n\tLog                  Log\n\tProfileMode          string\n\tProfilePath          string\n\tListen               []Listen\n\tMetrics              Metrics\n\tBGP                  BGP\n\tUI                   UI\n\tRegistry             Registry\n\tProxy                Proxy\n\tRuntime              Runtime\n\tGlobCacheSize        int\n\tInsecure             bool\n\tGlobMatchingDisabled bool\n}\n\ntype CertSource struct {\n\tHeader          http.Header\n\tName            string\n\tType            string\n\tCertPath        string\n\tKeyPath         string\n\tClientCAPath    string\n\tCAUpgradeCN     string\n\tVaultFetchToken string\n\tRefresh         time.Duration\n}\n\ntype Listen struct {\n\tCertSource         CertSource\n\tAddr               string\n\tProto              string\n\tTLSCiphers         []uint16\n\tReadTimeout        time.Duration\n\tWriteTimeout       time.Duration\n\tIdleTimeout        time.Duration\n\tProxyHeaderTimeout time.Duration\n\tRefresh            time.Duration\n\tTLSMinVersion      uint16\n\tTLSMaxVersion      uint16\n\tStrictMatch        bool\n\tProxyProto         bool\n}\n\ntype Source struct {\n\tScheme      string\n\tHost        string\n\tPort        string\n\tLinkEnabled bool\n\tNewTab      bool\n}\n\ntype RoutingTable struct {\n\tSource Source\n}\n\ntype UI struct {\n\tRoutingTable RoutingTable\n\tColor        string\n\tTitle        string\n\tAccess       string\n\tListen       Listen\n}\n\ntype Proxy struct {\n\tGZIPContentTypes      *regexp.Regexp\n\tAuthSchemes           map[string]AuthScheme\n\tStrategy              string\n\tMatcher               string\n\tLocalIP               string\n\tClientIPHeader        string\n\tTLSHeader             string\n\tTLSHeaderValue        string\n\tRequestID             string\n\tSTSHeader             STSHeader\n\tNoRouteStatus         int\n\tMaxConn               int\n\tShutdownWait          time.Duration\n\tDeregisterGracePeriod time.Duration\n\tDialTimeout           time.Duration\n\tResponseHeaderTimeout time.Duration\n\tKeepAliveTimeout      time.Duration\n\tIdleConnTimeout       time.Duration\n\tFlushInterval         time.Duration\n\tGlobalFlushInterval   time.Duration\n\tGRPCMaxRxMsgSize      int\n\tGRPCMaxTxMsgSize      int\n\tGRPCGShutdownTimeout  time.Duration\n}\n\ntype STSHeader struct {\n\tMaxAge     int\n\tSubdomains bool\n\tPreload    bool\n}\n\ntype Runtime struct {\n\tGOGC       int\n\tGOMAXPROCS int\n}\n\ntype Circonus struct {\n\tAPIKey        string\n\tAPIApp        string\n\tAPIURL        string\n\tCheckID       string\n\tBrokerID      string\n\tSubmissionURL string\n}\n\ntype Prometheus struct {\n\tSubsystem string\n\tPath      string\n\tBuckets   []float64\n}\n\ntype Log struct {\n\tAccessFormat string\n\tAccessTarget string\n\tRoutesFormat string\n\tLevel        string\n}\n\ntype Metrics struct {\n\tCirconus      Circonus\n\tTarget        string\n\tPrefix        string\n\tNames         string\n\tGraphiteAddr  string\n\tStatsDAddr    string\n\tDogstatsdAddr string\n\tPrometheus    Prometheus\n\tInterval      time.Duration\n\tTimeout       time.Duration\n\tRetry         time.Duration\n}\n\ntype Registry struct {\n\tStatic  Static\n\tFile    File\n\tBackend string\n\tCustom  Custom\n\tConsul  Consul\n\tTimeout time.Duration\n\tRetry   time.Duration\n}\n\ntype Static struct {\n\tNoRouteHTML string\n\tRoutes      string\n}\n\ntype File struct {\n\tNoRouteHTMLPath string\n\tRoutesPath      string\n}\n\ntype Consul struct {\n\tAddr               string\n\tScheme             string\n\tToken              string\n\tKVPath             string\n\tNoRouteHTMLPath    string\n\tTagPrefix          string\n\tNamespace          string\n\tServiceAddr        string\n\tServiceName        string\n\tCheckScheme        string\n\tChecksRequired     string\n\tTLS                ConsulTlS\n\tServiceTags        []string\n\tServiceStatus      []string\n\tCheckInterval      time.Duration\n\tCheckTimeout       time.Duration\n\tServiceMonitors    int\n\tPollInterval       time.Duration\n\tRegister           bool\n\tCheckTLSSkipVerify bool\n\tRequireConsistent  bool\n\tAllowStale         bool\n}\n\ntype Custom struct {\n\tHost               string\n\tPath               string\n\tQueryParams        string\n\tScheme             string\n\tNoRouteHTML        string\n\tPollInterval       time.Duration\n\tTimeout            time.Duration\n\tCheckTLSSkipVerify bool\n}\n\ntype AuthScheme struct {\n\tName  string\n\tType  string\n\tBasic BasicAuth\n}\n\ntype BasicAuth struct {\n\tModTime time.Time // the htpasswd file last modification time\n\tRealm   string\n\tFile    string\n\tRefresh time.Duration\n}\n\ntype ConsulTlS struct {\n\tKeyFile            string\n\tCertFile           string\n\tCAFile             string\n\tCAPath             string\n\tInsecureSkipVerify bool\n}\n\ntype BGP struct {\n\tRouterID          string\n\tGRPCListenAddress string\n\tCertFile          string\n\tKeyFile           string\n\tGOBGPDCfgFile     string\n\tNextHop           string\n\tAnycastAddresses  []string\n\tListenAddresses   []string\n\tPeers             []BGPPeer\n\tAsn               uint\n\tListenPort        int\n\tBGPEnabled        bool\n\tEnableGRPC        bool\n\tGRPCTLS           bool\n}\n\ntype BGPPeer struct {\n\tNeighborAddress string\n\tPassword        string\n\tNeighborPort    uint\n\tAsn             uint\n\tMultiHopLength  uint\n\tMultiHop        bool\n}\n"
  },
  {
    "path": "config/default.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n)\n\nvar defaultValues = struct {\n\tListenerValue         string\n\tCertSourcesValue      string\n\tAuthSchemesValue      string\n\tUIListenerValue       string\n\tGZIPContentTypesValue string\n\tBGPPeersValue         string\n\tReadTimeout           time.Duration\n\tWriteTimeout          time.Duration\n\tIdleTimeout           time.Duration\n}{\n\tListenerValue:   \":9999\",\n\tUIListenerValue: \":9998\",\n}\n\nvar defaultConfig = &Config{\n\tProfilePath: os.TempDir(),\n\tLog: Log{\n\t\tAccessFormat: \"common\",\n\t\tRoutesFormat: \"delta\",\n\t\tLevel:        \"INFO\",\n\t},\n\tMetrics: Metrics{\n\t\tPrefix:   \"{{clean .Hostname}}.{{clean .Exec}}\",\n\t\tNames:    \"{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}\",\n\t\tInterval: 30 * time.Second,\n\t\tTimeout:  10 * time.Second,\n\t\tRetry:    500 * time.Millisecond,\n\t\tCirconus: Circonus{\n\t\t\tAPIApp: \"fabio\",\n\t\t},\n\t\tPrometheus: Prometheus{\n\t\t\tBuckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},\n\t\t\tPath:    \"/metrics\",\n\t\t},\n\t},\n\tProxy: Proxy{\n\t\tMaxConn:              10000,\n\t\tStrategy:             \"rnd\",\n\t\tMatcher:              \"prefix\",\n\t\tNoRouteStatus:        404,\n\t\tDialTimeout:          30 * time.Second,\n\t\tFlushInterval:        time.Second,\n\t\tGlobalFlushInterval:  0,\n\t\tLocalIP:              LocalIPString(),\n\t\tAuthSchemes:          map[string]AuthScheme{},\n\t\tIdleConnTimeout:      15 * time.Second,\n\t\tGRPCMaxRxMsgSize:     4 * 1024 * 1024, // 4M\n\t\tGRPCMaxTxMsgSize:     4 * 1024 * 1024, // 4M\n\t\tGRPCGShutdownTimeout: time.Second * 2,\n\t},\n\tRegistry: Registry{\n\t\tBackend: \"consul\",\n\t\tConsul: Consul{\n\t\t\tAddr:              \"localhost:8500\",\n\t\t\tScheme:            \"http\",\n\t\t\tKVPath:            \"/fabio/config\",\n\t\t\tNoRouteHTMLPath:   \"/fabio/noroute.html\",\n\t\t\tTagPrefix:         \"urlprefix-\",\n\t\t\tRegister:          true,\n\t\t\tNamespace:         \"\",\n\t\t\tServiceAddr:       \":9998\",\n\t\t\tServiceName:       \"fabio\",\n\t\t\tServiceStatus:     []string{\"passing\"},\n\t\t\tServiceMonitors:   1,\n\t\t\tCheckInterval:     time.Second,\n\t\t\tCheckTimeout:      3 * time.Second,\n\t\t\tCheckScheme:       \"http\",\n\t\t\tChecksRequired:    \"one\",\n\t\t\tPollInterval:      0,\n\t\t\tRequireConsistent: true,\n\t\t\tAllowStale:        false,\n\t\t},\n\t\tCustom: Custom{\n\t\t\tHost:               \"\",\n\t\t\tScheme:             \"https\",\n\t\t\tCheckTLSSkipVerify: false,\n\t\t\tPollInterval:       5,\n\t\t\tNoRouteHTML:        \"\",\n\t\t\tTimeout:            10,\n\t\t\tPath:               \"\",\n\t\t\tQueryParams:        \"\",\n\t\t},\n\t\tTimeout: 10 * time.Second,\n\t\tRetry:   500 * time.Millisecond,\n\t},\n\tRuntime: Runtime{\n\t\tGOGC:       100,\n\t\tGOMAXPROCS: runtime.NumCPU(),\n\t},\n\tUI: UI{\n\t\tListen: Listen{\n\t\t\tAddr:  \":9998\",\n\t\t\tProto: \"http\",\n\t\t},\n\t\tColor:  \"light-green\",\n\t\tAccess: \"rw\",\n\t\tRoutingTable: RoutingTable{\n\t\t\tSource: Source{\n\t\t\t\tLinkEnabled: false,\n\t\t\t\tNewTab:      true,\n\t\t\t\tScheme:      \"http\",\n\t\t\t},\n\t\t},\n\t},\n\n\tGlobCacheSize: 1000,\n\n\tBGP: BGP{\n\t\tBGPEnabled:        false,\n\t\tAsn:               65000,\n\t\tAnycastAddresses:  nil,\n\t\tRouterID:          \"\",\n\t\tListenPort:        179,\n\t\tListenAddresses:   []string{\"0.0.0.0\"},\n\t\tPeers:             nil,\n\t\tEnableGRPC:        false,\n\t\tGRPCListenAddress: \"127.0.0.1:50051\",\n\t},\n}\n\nvar defaultBGPPeer = &BGPPeer{\n\tMultiHopLength: 2,\n}\n"
  },
  {
    "path": "config/flagset.go",
    "content": "package config\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/magiconair/properties\"\n)\n\n// -- stringSliceValue\ntype stringSliceValue []string\n\nfunc newStringSliceValue(val []string, p *[]string) *stringSliceValue {\n\t*p = val\n\treturn (*stringSliceValue)(p)\n}\n\nfunc (v *stringSliceValue) Set(s string) error {\n\t*v = []string{}\n\tfor _, x := range strings.Split(s, \",\") {\n\t\tx = strings.TrimSpace(x)\n\t\tif x == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t*v = append(*v, x)\n\t}\n\treturn nil\n}\n\nfunc (v *stringSliceValue) Get() interface{} { return []string(*v) }\nfunc (v *stringSliceValue) String() string   { return strings.Join(*v, \",\") }\n\ntype floatSliceValue []float64\n\nfunc newFloatSliceValue(val []float64, p *[]float64) *floatSliceValue {\n\t*p = val\n\treturn (*floatSliceValue)(p)\n}\n\nfunc (f *floatSliceValue) String() string {\n\tstrs := make([]string, len(*f))\n\tfor i, v := range *f {\n\t\tstrs[i] = strconv.FormatFloat(v, 'f', -1, 64)\n\t}\n\treturn strings.Join(strs, \",\")\n}\n\nfunc (f *floatSliceValue) Set(s string) error {\n\t*f = []float64{}\n\tfor _, x := range strings.Split(s, \",\") {\n\t\tx = strings.TrimSpace(x)\n\t\tif x == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tv, err := strconv.ParseFloat(x, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error parsing float slice value %s: %w\", x, err)\n\t\t}\n\t\t*f = append(*f, v)\n\t}\n\treturn nil\n}\n\n// -- FlagSet\ntype FlagSet struct {\n\tflag.FlagSet\n\tset map[string]bool\n}\n\nfunc NewFlagSet(name string, errorHandling flag.ErrorHandling) *FlagSet {\n\tfs := &FlagSet{set: make(map[string]bool)}\n\tfs.Init(name, errorHandling)\n\treturn fs\n}\n\n// IsSet returns true if a variable was set via any mechanism.\nfunc (f *FlagSet) IsSet(name string) bool {\n\treturn f.set[name]\n}\n\nfunc (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) {\n\tf.Var(newStringSliceValue(value, p), name, usage)\n}\n\nfunc (f *FlagSet) FloatSliceVar(p *[]float64, name string, value []float64, usage string) {\n\tf.Var(newFloatSliceValue(value, p), name, usage)\n}\n\n// ParseFlags parses command line arguments and provides fallback\n// values from environment variables and config file values.\n// Environment variables are case-insensitive and can have either\n// of the provided prefixes.\nfunc (f *FlagSet) ParseFlags(args, environ, prefixes []string, p *properties.Properties) error {\n\tif err := f.Parse(args); err != nil {\n\t\treturn err\n\t}\n\n\tif len(prefixes) == 0 {\n\t\tprefixes = []string{\"\"}\n\t}\n\n\t// parse environment in case-insensitive way\n\tenv := map[string]string{}\n\tfor _, e := range environ {\n\t\tp := strings.SplitN(e, \"=\", 2)\n\t\tenv[strings.ToUpper(p[0])] = p[1]\n\t}\n\n\t// determine all values that were set via cmdline\n\tf.Visit(func(fl *flag.Flag) {\n\t\tf.set[fl.Name] = true\n\t})\n\n\t// lookup the rest via environ and properties\n\tf.VisitAll(func(fl *flag.Flag) {\n\t\t// skip if already set\n\t\tif f.set[fl.Name] {\n\t\t\treturn\n\t\t}\n\n\t\t// check environment variables\n\t\tfor _, pfx := range prefixes {\n\t\t\tname := strings.ToUpper(pfx + strings.ReplaceAll(fl.Name, \".\", \"_\"))\n\t\t\tif val, ok := env[name]; ok {\n\t\t\t\tf.set[fl.Name] = true\n\t\t\t\tf.Set(fl.Name, val)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// check properties\n\t\tif p == nil {\n\t\t\treturn\n\t\t}\n\t\tif val, ok := p.Get(fl.Name); ok {\n\t\t\tf.set[fl.Name] = true\n\t\t\tf.Set(fl.Name, val)\n\t\t\treturn\n\t\t}\n\t})\n\treturn nil\n}\n"
  },
  {
    "path": "config/flagset_test.go",
    "content": "package config\n\nimport (\n\t\"flag\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/magiconair/properties\"\n)\n\nfunc TestParseFlags(t *testing.T) {\n\tprops := func(s string) *properties.Properties {\n\t\treturn properties.MustLoadString(s)\n\t}\n\n\ttests := []struct {\n\t\tdesc   string\n\t\targs   []string\n\t\tenv    []string\n\t\tprefix []string\n\t\tprops  string\n\t\ta      []string\n\t\tkv     map[string]string\n\t\tkvs    []map[string]string\n\t\tv      string\n\t}{\n\t\t{\n\t\t\tdesc:  \"cmdline should win\",\n\t\t\targs:  []string{\"-v\", \"cmdline\"},\n\t\t\tenv:   []string{\"v=env\"},\n\t\t\tprops: \"v=props\",\n\t\t\tv:     \"cmdline\",\n\t\t},\n\t\t{\n\t\t\tdesc:  \"env should win\",\n\t\t\tenv:   []string{\"v=env\"},\n\t\t\tprops: \"v=props\",\n\t\t\tv:     \"env\",\n\t\t},\n\t\t{\n\t\t\tdesc:   \"env with prefix should win\",\n\t\t\tenv:    []string{\"v=env\", \"p_v=prefix\"},\n\t\t\tprefix: []string{\"p_\"},\n\t\t\tprops:  \"v=props\",\n\t\t\tv:      \"prefix\",\n\t\t},\n\t\t{\n\t\t\tdesc:  \"props should win\",\n\t\t\tprops: \"v=props\",\n\t\t\tv:     \"props\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"string slice in cmdline\",\n\t\t\targs: []string{\"-a\", \"1,2,3\"},\n\t\t\ta:    []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"string slice in env\",\n\t\t\tenv:  []string{\"a=1,2,3\"},\n\t\t\ta:    []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"string slice in props\",\n\t\t\tprops: \"a=1,2,3\",\n\t\t\ta:     []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tvar a []string\n\t\t\tvar v string\n\t\t\tf := NewFlagSet(\"test\", flag.ExitOnError)\n\t\t\tf.StringVar(&v, \"v\", \"\", \"\")\n\t\t\tf.StringSliceVar(&a, \"a\", nil, \"\")\n\t\t\terr := f.ParseFlags(tt.args, tt.env, tt.prefix, props(tt.props))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"got %v want nil\", err)\n\t\t\t}\n\t\t\tif got, want := v, tt.v; got != want {\n\t\t\t\tt.Errorf(\"got %q want %q\", got, want)\n\t\t\t}\n\t\t\tif got, want := a, tt.a; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaults(t *testing.T) {\n\ta, aDefault := []string{}, []string{\"x\"}\n\tv, vDefault := \"\", \"x\"\n\n\tf := NewFlagSet(\"test\", flag.ExitOnError)\n\tf.StringVar(&v, \"v\", vDefault, \"\")\n\tf.StringSliceVar(&a, \"a\", aDefault, \"\")\n\n\tif got, want := v, vDefault; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := a, aDefault; !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "config/kvslice.go",
    "content": "package config\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// parseKVSlice parses a configuration string in the form\n//\n//\tkey=val;key=val,key=val;key=val\n//\n// into a list of string maps. maps are separated by comma and key/value\n// pairs within a map are separated by semicolons. The first key/value\n// pair of a map can omit the key and its value will be stored under the\n// empty key. This allows support of legacy configuration formats which\n// are\n//\n//\tval;opt1=val1;opt2=val2;...\nfunc parseKVSlice(in string) ([]map[string]string, error) {\n\tvar keyOrFirstVal string\n\tmaps := []map[string]string{}\n\tm := map[string]string{}\n\n\tnewMap := func() {\n\t\tif len(m) > 0 {\n\t\t\tmaps = append(maps, m)\n\t\t\tm = map[string]string{}\n\t\t}\n\t}\n\n\tv := \"\"\n\ts := []rune(in)\n\tstate := stateFirstKey\n\tfor len(s) > 0 {\n\t\ttyp, val, n := lex(s)\n\t\ts = s[n:]\n\t\t// fmt.Println(\"parse:\", \"typ:\", typ, \"val:\", val, \"v:\", v, \"state:\", string(state), \"s:\", string(s))\n\t\tswitch state {\n\t\tcase stateFirstKey:\n\t\t\tswitch typ {\n\t\t\tcase itemText:\n\t\t\t\tkeyOrFirstVal = strings.TrimSpace(val)\n\t\t\t\tstate = stateAfterFirstKey\n\t\t\tcase itemComma, itemSemicolon:\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(val)\n\t\t\t}\n\n\t\t// the first value is allowed to omit the key\n\t\t// a=b;c=d and b;c=d are valid\n\t\tcase stateAfterFirstKey:\n\t\t\tswitch typ {\n\t\t\tcase itemEqual:\n\t\t\t\tstate = stateVal\n\t\t\tcase itemComma:\n\t\t\t\tif keyOrFirstVal != \"\" {\n\t\t\t\t\tm[\"\"] = keyOrFirstVal\n\t\t\t\t}\n\t\t\t\tnewMap()\n\t\t\t\tstate = stateFirstKey\n\t\t\tcase itemSemicolon:\n\t\t\t\tif keyOrFirstVal != \"\" {\n\t\t\t\t\tm[\"\"] = keyOrFirstVal\n\t\t\t\t}\n\t\t\t\tstate = stateKey\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(val)\n\t\t\t}\n\n\t\tcase stateKey:\n\t\t\tswitch typ {\n\t\t\tcase itemText:\n\t\t\t\tkeyOrFirstVal = strings.TrimSpace(val)\n\t\t\t\tstate = stateEqual\n\t\t\tcase itemComma, itemSemicolon:\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(val)\n\t\t\t}\n\n\t\tcase stateEqual:\n\t\t\tswitch typ {\n\t\t\tcase itemEqual:\n\t\t\t\tstate = stateVal\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(val)\n\t\t\t}\n\n\t\tcase stateVal:\n\t\t\tswitch typ {\n\t\t\tcase itemText, itemEqual:\n\t\t\t\tv += val\n\t\t\tcase itemComma:\n\t\t\t\tm[keyOrFirstVal] = v\n\t\t\t\tv = \"\"\n\t\t\t\tnewMap()\n\t\t\t\tstate = stateFirstKey\n\t\t\tcase itemSemicolon:\n\t\t\t\tm[keyOrFirstVal] = v\n\t\t\t\tv = \"\"\n\t\t\t\tstate = stateKey\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(val)\n\t\t\t}\n\t\t}\n\t}\n\tswitch state {\n\tcase stateVal:\n\t\tm[keyOrFirstVal] = v\n\tcase stateAfterFirstKey:\n\t\tif keyOrFirstVal != \"\" {\n\t\t\tm[\"\"] = keyOrFirstVal\n\t\t}\n\t}\n\tif len(m) > 0 {\n\t\tmaps = append(maps, m)\n\t}\n\tif len(maps) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn maps, nil\n}\n\ntype itemType string\n\nconst (\n\titemText      itemType = \"TEXT\"\n\titemEqual     itemType = \"EQUAL\"\n\titemSemicolon itemType = \"SEMICOLON\"\n\titemComma     itemType = \"COMMA\"\n\titemError     itemType = \"ERROR\"\n)\n\nfunc (t itemType) String() string {\n\treturn string(t)\n}\n\ntype state string\n\nconst (\n\n\t// lexer states\n\tstateStart    state = \"start\"\n\tstateText     state = \"text\"\n\tstateQText    state = \"qtext\"\n\tstateQTextEnd state = \"qtextend\"\n\tstateQTextEsc state = \"qtextesc\"\n\n\t// parser states\n\tstateFirstKey      state = \"first-key\"\n\tstateKey           state = \"key\"\n\tstateEqual         state = \"equal\"\n\tstateVal           state = \"val\"\n\tstateAfterFirstKey state = \"equal-comma-semicolon\"\n)\n\nfunc lex(s []rune) (itemType, string, int) {\n\tisComma := func(r rune) bool { return r == ',' }\n\tisSemicolon := func(r rune) bool { return r == ';' }\n\tisEqual := func(r rune) bool { return r == '=' }\n\tisEscape := func(r rune) bool { return r == '\\\\' }\n\tisQuote := func(r rune) bool { return r == '\"' || r == '\\'' }\n\n\tvar quote rune\n\tstate := stateStart\n\tfor i, r := range s {\n\t\t// fmt.Println(\"lex:\", \"i:\", i, \"r:\", string(r), \"state:\", string(state))\n\t\tswitch state {\n\t\tcase stateStart:\n\t\t\tswitch {\n\t\t\tcase isComma(r):\n\t\t\t\treturn itemComma, string(r), 1\n\t\t\tcase isSemicolon(r):\n\t\t\t\treturn itemSemicolon, string(r), 1\n\t\t\tcase isEqual(r):\n\t\t\t\treturn itemEqual, string(r), 1\n\t\t\tcase isQuote(r):\n\t\t\t\tquote = r\n\t\t\t\tstate = stateQText\n\t\t\tdefault:\n\t\t\t\tstate = stateText\n\t\t\t}\n\t\tcase stateText:\n\t\t\tswitch {\n\t\t\tcase isComma(r) || isSemicolon(r) || isEqual(r):\n\t\t\t\treturn itemText, string(s[:i]), i\n\t\t\tdefault:\n\t\t\t\t// state = stateText\n\t\t\t}\n\t\tcase stateQText:\n\t\t\tswitch {\n\t\t\tcase r == quote:\n\t\t\t\tstate = stateQTextEnd\n\t\t\tcase isEscape(r):\n\t\t\t\tstate = stateQTextEsc\n\t\t\tdefault:\n\t\t\t\t// state = stateQText\n\t\t\t}\n\n\t\tcase stateQTextEsc:\n\t\t\tstate = stateQText\n\n\t\tcase stateQTextEnd:\n\t\t\tv, err := strconv.Unquote(string(s[:i]))\n\t\t\tif err != nil {\n\t\t\t\treturn itemError, \"invalid escape sequence\", i\n\t\t\t}\n\t\t\treturn itemText, v, i\n\t\t}\n\t}\n\n\t// fmt.Println(\"lex:\", \"state:\", string(state))\n\tswitch state {\n\tcase stateQText:\n\t\treturn itemError, \"unbalanced quotes\", len(s)\n\tcase stateQTextEsc:\n\t\treturn itemError, \"unterminated escape sequence\", len(s)\n\tcase stateQTextEnd:\n\t\tv, err := strconv.Unquote(string(s))\n\t\tif err != nil {\n\t\t\treturn itemError, \"invalid escape sequence\", len(s)\n\t\t}\n\t\treturn itemText, v, len(s)\n\tdefault:\n\t\treturn itemText, string(s), len(s)\n\t}\n}\n"
  },
  {
    "path": "config/kvslice_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestParseKVSlice(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\ts    string\n\t\tm    []map[string]string\n\t\terr  error\n\t}{\n\t\t{\"empty\", \"\", nil, nil},\n\t\t{\"key=val\", \"a=b\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"key with spaces\", \" a =b\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"quoted value\", \"a=\\\"b\\\"\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"single quoted value\", \"a='b'\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"quoted value with backslash\", `a=\"b\\\\\\\"\"`, []map[string]string{{\"a\": `b\\\"`}}, nil},\n\t\t{\"ignore empty map front\", \",a=b\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"ignore empty map back\", \"a=b,\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"ignore empty value front\", \";a=b\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"ignore empty value back\", \"a=b;\", []map[string]string{{\"a\": \"b\"}}, nil},\n\t\t{\"multiple values\", \"a=b;c=d\", []map[string]string{{\"a\": \"b\", \"c\": \"d\"}}, nil},\n\t\t{\"multiple maps\", \"a=b,c=d\", []map[string]string{{\"a\": \"b\"}, {\"c\": \"d\"}}, nil},\n\t\t{\"multiple values and maps\", \"a=b;c=d,e=f;g=h\", []map[string]string{{\"a\": \"b\", \"c\": \"d\"}, {\"e\": \"f\", \"g\": \"h\"}}, nil},\n\t\t{\"first key empty\", \"b\", []map[string]string{{\"\": \"b\"}}, nil},\n\t\t{\"first key empty and more values\", \"b;c=d\", []map[string]string{{\"\": \"b\", \"c\": \"d\"}}, nil},\n\t\t{\"first key empty and more maps\", \"b,c\", []map[string]string{{\"\": \"b\"}, {\"\": \"c\"}}, nil},\n\t\t{\"first key empty and more maps and values\", \"b;c=d,e;f=g\", []map[string]string{{\"\": \"b\", \"c\": \"d\"}, {\"\": \"e\", \"f\": \"g\"}}, nil},\n\t\t{\"issue 305\", \"a=b=c,d=e=f\", []map[string]string{{\"a\": \"b=c\"}, {\"d\": \"e=f\"}}, nil},\n\t\t{\"issue 305\", \"a=b=c;d=e=f\", []map[string]string{{\"a\": \"b=c\", \"d\": \"e=f\"}}, nil},\n\t\t{\"issue 305\", \"a=b;d=e=f\", []map[string]string{{\"a\": \"b\", \"d\": \"e=f\"}}, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tm, err := parseKVSlice(tt.s)\n\t\t\tif got, want := err, tt.err; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"got error %v want %v\", got, want)\n\t\t\t}\n\t\t\tif got, want := m, tt.m; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"got %#v want %#v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/load.go",
    "content": "package config\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tgs \"github.com/hashicorp/go-sockaddr/template\"\n\t\"github.com/magiconair/properties\"\n)\n\nvar tlsciphers map[string]uint16\n\nfunc loadCiphers() {\n\ttlsciphers = make(map[string]uint16)\n\tfor _, c := range tls.CipherSuites() {\n\t\ttlsciphers[c.Name] = c.ID\n\t}\n\tfor _, c := range tls.InsecureCipherSuites() {\n\t\ttlsciphers[c.Name] = c.ID\n\t}\n}\n\nfunc Load(args, environ []string) (cfg *Config, err error) {\n\tvar props *properties.Properties\n\tloadCiphers()\n\tcmdline, path, version, err := parse(args)\n\tswitch {\n\tcase err != nil:\n\t\treturn nil, err\n\tcase version:\n\t\treturn nil, nil\n\tcase path != \"\":\n\t\tswitch {\n\t\tcase strings.HasPrefix(path, \"http://\") || strings.HasPrefix(path, \"https://\"):\n\t\t\tprops, err = properties.LoadURL(path)\n\t\tcase path != \"\":\n\t\t\tprops, err = properties.LoadFile(path, properties.UTF8)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tenvprefix := []string{\"FABIO_\", \"\"}\n\treturn load(cmdline, environ, envprefix, props)\n}\n\nvar errInvalidConfig = errors.New(\"invalid or missing path to config file\")\n\n// parse extracts the version and config file flags from the command\n// line arguments and returns the individual parts. Test flags are\n// ignored.\nfunc parse(args []string) (cmdline []string, path string, version bool, err error) {\n\tif len(args) < 1 {\n\t\tpanic(\"missing exec name\")\n\t}\n\n\t// always copy the name of the executable\n\tcmdline = args[:1]\n\n\t// parse rest of the arguments\n\tfor i := 1; i < len(args); i++ {\n\t\targ := args[i]\n\n\t\tswitch {\n\t\t// version flag\n\t\tcase arg == \"-v\" || arg == \"-version\" || arg == \"--version\":\n\t\t\treturn nil, \"\", true, nil\n\n\t\t// config file without '='\n\t\tcase arg == \"-cfg\" || arg == \"--cfg\":\n\t\t\tif i >= len(args)-1 {\n\t\t\t\treturn nil, \"\", false, errInvalidConfig\n\t\t\t}\n\t\t\tpath = args[i+1]\n\t\t\ti++\n\n\t\t// config file with '='. needs unquoting\n\t\tcase strings.HasPrefix(arg, \"-cfg=\") || strings.HasPrefix(arg, \"--cfg=\"):\n\t\t\tif strings.HasPrefix(arg, \"-cfg=\") {\n\t\t\t\tpath = arg[len(\"-cfg=\"):]\n\t\t\t} else {\n\t\t\t\tpath = arg[len(\"--cfg=\"):]\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase path == \"\":\n\t\t\t\treturn nil, \"\", false, errInvalidConfig\n\t\t\tcase path[0] == '\\'':\n\t\t\t\tpath = strings.Trim(path, \"'\")\n\t\t\tcase path[0] == '\"':\n\t\t\t\tpath = strings.Trim(path, \"\\\"\")\n\t\t\t}\n\t\t\tif path == \"\" {\n\t\t\t\treturn nil, \"\", false, errInvalidConfig\n\t\t\t}\n\n\t\t// ignore test flags\n\t\tcase strings.HasPrefix(arg, \"-test.\"):\n\t\t\tcontinue\n\n\t\tdefault:\n\t\t\tcmdline = append(cmdline, arg)\n\t\t}\n\t}\n\treturn cmdline, path, false, nil\n}\n\nfunc load(cmdline, environ, envprefix []string, props *properties.Properties) (cfg *Config, err error) {\n\tcfg = &Config{}\n\tf := NewFlagSet(cmdline[0], flag.ExitOnError)\n\n\t// dummy values which were parsed earlier\n\tf.String(\"cfg\", \"\", \"Path or URL to config file\")\n\tf.Bool(\"v\", false, \"Show version\")\n\tf.Bool(\"version\", false, \"Show version\")\n\n\t// config values\n\tvar listenerValue string\n\tvar uiListenerValue string\n\tvar certSourcesValue string\n\tvar authSchemesValue string\n\tvar readTimeout, writeTimeout time.Duration\n\tvar gzipContentTypesValue string\n\n\tvar obsoleteStr string\n\n\tvar bgpPeersValue string\n\n\tf.BoolVar(&cfg.Insecure, \"insecure\", defaultConfig.Insecure, \"allow fabio to run as root when set to true\")\n\tf.IntVar(&cfg.Proxy.MaxConn, \"proxy.maxconn\", defaultConfig.Proxy.MaxConn, \"maximum number of cached connections\")\n\tf.StringVar(&cfg.Proxy.Strategy, \"proxy.strategy\", defaultConfig.Proxy.Strategy, \"load balancing strategy\")\n\tf.StringVar(&cfg.Proxy.Matcher, \"proxy.matcher\", defaultConfig.Proxy.Matcher, \"path matching algorithm\")\n\tf.IntVar(&cfg.Proxy.NoRouteStatus, \"proxy.noroutestatus\", defaultConfig.Proxy.NoRouteStatus, \"status code for invalid route. Must be three digits\")\n\tf.DurationVar(&cfg.Proxy.ShutdownWait, \"proxy.shutdownwait\", defaultConfig.Proxy.ShutdownWait, \"time for graceful shutdown\")\n\tf.DurationVar(&cfg.Proxy.DeregisterGracePeriod, \"proxy.deregistergraceperiod\", defaultConfig.Proxy.DeregisterGracePeriod, \"time to wait after deregistering from a registry\")\n\tf.DurationVar(&cfg.Proxy.DialTimeout, \"proxy.dialtimeout\", defaultConfig.Proxy.DialTimeout, \"connection timeout for backend connections\")\n\tf.DurationVar(&cfg.Proxy.ResponseHeaderTimeout, \"proxy.responseheadertimeout\", defaultConfig.Proxy.ResponseHeaderTimeout, \"response header timeout\")\n\tf.DurationVar(&cfg.Proxy.KeepAliveTimeout, \"proxy.keepalivetimeout\", defaultConfig.Proxy.KeepAliveTimeout, \"keep-alive timeout\")\n\tf.DurationVar(&cfg.Proxy.IdleConnTimeout, \"proxy.idleconntimeout\", defaultConfig.Proxy.IdleConnTimeout, \"idle timeout, when to close (keep-alive) connections\")\n\tf.StringVar(&cfg.Proxy.LocalIP, \"proxy.localip\", defaultConfig.Proxy.LocalIP, \"fabio address in Forward headers\")\n\tf.StringVar(&cfg.Proxy.ClientIPHeader, \"proxy.header.clientip\", defaultConfig.Proxy.ClientIPHeader, \"header for the request ip\")\n\tf.StringVar(&cfg.Proxy.TLSHeader, \"proxy.header.tls\", defaultConfig.Proxy.TLSHeader, \"header for TLS connections\")\n\tf.StringVar(&cfg.Proxy.TLSHeaderValue, \"proxy.header.tls.value\", defaultConfig.Proxy.TLSHeaderValue, \"value for TLS connection header\")\n\tf.StringVar(&cfg.Proxy.RequestID, \"proxy.header.requestid\", defaultConfig.Proxy.RequestID, \"header for reqest id\")\n\tf.IntVar(&cfg.Proxy.STSHeader.MaxAge, \"proxy.header.sts.maxage\", defaultConfig.Proxy.STSHeader.MaxAge, \"enable and set the max-age value for HSTS\")\n\tf.BoolVar(&cfg.Proxy.STSHeader.Subdomains, \"proxy.header.sts.subdomains\", defaultConfig.Proxy.STSHeader.Subdomains, \"direct HSTS to include subdomains\")\n\tf.BoolVar(&cfg.Proxy.STSHeader.Preload, \"proxy.header.sts.preload\", defaultConfig.Proxy.STSHeader.Preload, \"direct HSTS to pass the preload directive\")\n\tf.IntVar(&cfg.Proxy.GRPCMaxRxMsgSize, \"proxy.grpcmaxrxmsgsize\", defaultConfig.Proxy.GRPCMaxRxMsgSize, \"max grpc receive message size (in bytes)\")\n\tf.IntVar(&cfg.Proxy.GRPCMaxTxMsgSize, \"proxy.grpcmaxtxmsgsize\", defaultConfig.Proxy.GRPCMaxTxMsgSize, \"max grpc transmit message size (in bytes)\")\n\tf.DurationVar(&cfg.Proxy.GRPCGShutdownTimeout, \"proxy.grpcshutdowntimeout\", defaultConfig.Proxy.GRPCGShutdownTimeout, \"amount of time to wait for graceful shutdown of grpc backend\")\n\tf.StringVar(&gzipContentTypesValue, \"proxy.gzip.contenttype\", defaultValues.GZIPContentTypesValue, \"regexp of content types to compress\")\n\tf.StringVar(&listenerValue, \"proxy.addr\", defaultValues.ListenerValue, \"listener config\")\n\tf.StringVar(&certSourcesValue, \"proxy.cs\", defaultValues.CertSourcesValue, \"certificate sources\")\n\tf.DurationVar(&readTimeout, \"proxy.readtimeout\", defaultValues.ReadTimeout, \"read timeout for incoming requests\")\n\tf.DurationVar(&writeTimeout, \"proxy.writetimeout\", defaultValues.WriteTimeout, \"write timeout for outgoing responses\")\n\tf.DurationVar(&cfg.Proxy.FlushInterval, \"proxy.flushinterval\", defaultConfig.Proxy.FlushInterval, \"flush interval for streaming responses\")\n\tf.DurationVar(&cfg.Proxy.GlobalFlushInterval, \"proxy.globalflushinterval\", defaultConfig.Proxy.GlobalFlushInterval, \"flush interval for non-streaming responses\")\n\tf.StringVar(&authSchemesValue, \"proxy.auth\", defaultValues.AuthSchemesValue, \"auth schemes\")\n\tf.StringVar(&cfg.Log.AccessFormat, \"log.access.format\", defaultConfig.Log.AccessFormat, \"access log format\")\n\tf.StringVar(&cfg.Log.AccessTarget, \"log.access.target\", defaultConfig.Log.AccessTarget, \"access log target\")\n\tf.StringVar(&cfg.Log.RoutesFormat, \"log.routes.format\", defaultConfig.Log.RoutesFormat, \"log format of routing table updates\")\n\tf.StringVar(&cfg.Log.Level, \"log.level\", defaultConfig.Log.Level, \"log level: TRACE, DEBUG, INFO, WARN, ERROR, FATAL\")\n\tf.StringVar(&cfg.Metrics.Target, \"metrics.target\", defaultConfig.Metrics.Target, \"metrics backend\")\n\tf.StringVar(&cfg.Metrics.Prefix, \"metrics.prefix\", defaultConfig.Metrics.Prefix, \"prefix for reported metrics\")\n\tf.StringVar(&cfg.Metrics.Names, \"metrics.names\", defaultConfig.Metrics.Names, \"route metric name template\")\n\tf.DurationVar(&cfg.Metrics.Interval, \"metrics.interval\", defaultConfig.Metrics.Interval, \"metrics reporting interval\")\n\tf.DurationVar(&cfg.Metrics.Timeout, \"metrics.timeout\", defaultConfig.Metrics.Timeout, \"timeout for metrics to become available\")\n\tf.DurationVar(&cfg.Metrics.Retry, \"metrics.retry\", defaultConfig.Metrics.Retry, \"retry interval during startup\")\n\tf.StringVar(&cfg.Metrics.GraphiteAddr, \"metrics.graphite.addr\", defaultConfig.Metrics.GraphiteAddr, \"graphite server address\")\n\tf.StringVar(&cfg.Metrics.StatsDAddr, \"metrics.statsd.addr\", defaultConfig.Metrics.StatsDAddr, \"statsd server address\")\n\tf.StringVar(&cfg.Metrics.DogstatsdAddr, \"metrics.dogstatsd.addr\", defaultConfig.Metrics.DogstatsdAddr, \"dogstatsd server address\")\n\tf.StringVar(&cfg.Metrics.Circonus.APIKey, \"metrics.circonus.apikey\", defaultConfig.Metrics.Circonus.APIKey, \"Circonus API token key\")\n\tf.StringVar(&cfg.Metrics.Circonus.APIApp, \"metrics.circonus.apiapp\", defaultConfig.Metrics.Circonus.APIApp, \"Circonus API token app\")\n\tf.StringVar(&cfg.Metrics.Circonus.APIURL, \"metrics.circonus.apiurl\", defaultConfig.Metrics.Circonus.APIURL, \"Circonus API URL\")\n\tf.StringVar(&cfg.Metrics.Circonus.BrokerID, \"metrics.circonus.brokerid\", defaultConfig.Metrics.Circonus.BrokerID, \"Circonus Broker ID\")\n\tf.StringVar(&cfg.Metrics.Circonus.CheckID, \"metrics.circonus.checkid\", defaultConfig.Metrics.Circonus.CheckID, \"Circonus Check ID\")\n\tf.StringVar(&cfg.Metrics.Circonus.SubmissionURL, \"metrics.circonus.submissionurl\", defaultConfig.Metrics.Circonus.SubmissionURL, \"Circonus Check SubmissionURL\")\n\tf.StringVar(&cfg.Metrics.Prometheus.Subsystem, \"metrics.prometheus.subsystem\", defaultConfig.Metrics.Prometheus.Subsystem, \"Prometheus system\")\n\tf.StringVar(&cfg.Metrics.Prometheus.Path, \"metrics.prometheus.path\", defaultConfig.Metrics.Prometheus.Path, \"Prometheus http handler path\")\n\tf.FloatSliceVar(&cfg.Metrics.Prometheus.Buckets, \"metrics.prometheus.buckets\", defaultConfig.Metrics.Prometheus.Buckets, \"Prometheus histogram buckets\")\n\tf.StringVar(&cfg.Registry.Backend, \"registry.backend\", defaultConfig.Registry.Backend, \"registry backend\")\n\tf.DurationVar(&cfg.Registry.Timeout, \"registry.timeout\", defaultConfig.Registry.Timeout, \"timeout for registry to become available\")\n\tf.DurationVar(&cfg.Registry.Retry, \"registry.retry\", defaultConfig.Registry.Retry, \"retry interval during startup\")\n\tf.StringVar(&cfg.Registry.File.RoutesPath, \"registry.file.path\", defaultConfig.Registry.File.RoutesPath, \"path to file based routing table\")\n\tf.StringVar(&cfg.Registry.File.NoRouteHTMLPath, \"registry.file.noroutehtmlpath\", defaultConfig.Registry.File.NoRouteHTMLPath, \"path to file for HTML returned when no route is found\")\n\tf.StringVar(&cfg.Registry.Static.Routes, \"registry.static.routes\", defaultConfig.Registry.Static.Routes, \"static routes\")\n\tf.StringVar(&cfg.Registry.Static.NoRouteHTML, \"registry.static.noroutehtml\", defaultConfig.Registry.Static.NoRouteHTML, \"HTML which is returned when no route is found\")\n\tf.StringVar(&cfg.Registry.Consul.Addr, \"registry.consul.addr\", defaultConfig.Registry.Consul.Addr, \"address of the consul agent\")\n\tf.StringVar(&cfg.Registry.Consul.Token, \"registry.consul.token\", defaultConfig.Registry.Consul.Token, \"token for consul agent\")\n\tf.StringVar(&cfg.Registry.Consul.KVPath, \"registry.consul.kvpath\", defaultConfig.Registry.Consul.KVPath, \"consul KV path for manual overrides\")\n\tf.StringVar(&cfg.Registry.Consul.NoRouteHTMLPath, \"registry.consul.noroutehtmlpath\", defaultConfig.Registry.Consul.NoRouteHTMLPath, \"consul KV path for HTML returned when no route is found\")\n\tf.StringVar(&cfg.Registry.Consul.TagPrefix, \"registry.consul.tagprefix\", defaultConfig.Registry.Consul.TagPrefix, \"prefix for consul tags\")\n\tf.StringVar(&cfg.Registry.Consul.TLS.KeyFile, \"registry.consul.tls.keyfile\", defaultConfig.Registry.Consul.TLS.KeyFile, \"path to consul key file\")\n\tf.StringVar(&cfg.Registry.Consul.TLS.CertFile, \"registry.consul.tls.certfile\", defaultConfig.Registry.Consul.TLS.CertFile, \"path to consul cert file\")\n\tf.StringVar(&cfg.Registry.Consul.TLS.CAFile, \"registry.consul.tls.cafile\", defaultConfig.Registry.Consul.TLS.CAFile, \"path to consul CA file\")\n\tf.StringVar(&cfg.Registry.Consul.TLS.CAPath, \"registry.consul.tls.capath\", defaultConfig.Registry.Consul.TLS.CAPath, \"path to consul CA directory\")\n\tf.BoolVar(&cfg.Registry.Consul.TLS.InsecureSkipVerify, \"registry.consul.tls.insecureskipverify\", defaultConfig.Registry.Consul.TLS.InsecureSkipVerify, \"is tls check enabled\")\n\tf.BoolVar(&cfg.Registry.Consul.Register, \"registry.consul.register.enabled\", defaultConfig.Registry.Consul.Register, \"register fabio in consul\")\n\tf.StringVar(&cfg.Registry.Consul.Namespace, \"registry.consul.namespace\", defaultConfig.Registry.Consul.Namespace, \"consul namespace in which fabio is active\")\n\tf.StringVar(&cfg.Registry.Consul.ServiceAddr, \"registry.consul.register.addr\", \"<ui.addr>\", \"service registration address\")\n\tf.StringVar(&cfg.Registry.Consul.ServiceName, \"registry.consul.register.name\", defaultConfig.Registry.Consul.ServiceName, \"service registration name\")\n\tf.StringSliceVar(&cfg.Registry.Consul.ServiceTags, \"registry.consul.register.tags\", defaultConfig.Registry.Consul.ServiceTags, \"service registration tags\")\n\tf.StringSliceVar(&cfg.Registry.Consul.ServiceStatus, \"registry.consul.service.status\", defaultConfig.Registry.Consul.ServiceStatus, \"valid service status values\")\n\tf.DurationVar(&cfg.Registry.Consul.CheckInterval, \"registry.consul.register.checkInterval\", defaultConfig.Registry.Consul.CheckInterval, \"service check interval\")\n\tf.DurationVar(&cfg.Registry.Consul.CheckTimeout, \"registry.consul.register.checkTimeout\", defaultConfig.Registry.Consul.CheckTimeout, \"service check timeout\")\n\tf.BoolVar(&cfg.Registry.Consul.CheckTLSSkipVerify, \"registry.consul.register.checkTLSSkipVerify\", defaultConfig.Registry.Consul.CheckTLSSkipVerify, \"service check TLS verification\")\n\tf.StringVar(&obsoleteStr, \"registry.consul.register.checkDeregisterCriticalServiceAfter\", \"\", \"This option is deprecated and has no effect.\")\n\tf.StringVar(&cfg.Registry.Consul.ChecksRequired, \"registry.consul.checksRequired\", defaultConfig.Registry.Consul.ChecksRequired, \"number of checks which must pass: one or all\")\n\tf.IntVar(&cfg.Registry.Consul.ServiceMonitors, \"registry.consul.serviceMonitors\", defaultConfig.Registry.Consul.ServiceMonitors, \"concurrency for route updates\")\n\tf.DurationVar(&cfg.Registry.Consul.PollInterval, \"registry.consul.pollinterval\", defaultConfig.Registry.Consul.PollInterval, \"poll interval for route updates\")\n\tf.BoolVar(&cfg.Registry.Consul.RequireConsistent, \"registry.consul.requireConsistent\", defaultConfig.Registry.Consul.RequireConsistent, \"is consistent read mode on consul queries required\")\n\tf.BoolVar(&cfg.Registry.Consul.AllowStale, \"registry.consul.allowStale\", defaultConfig.Registry.Consul.AllowStale, \"is stale read mode on consul queries allowed\")\n\tf.IntVar(&cfg.Runtime.GOGC, \"runtime.gogc\", defaultConfig.Runtime.GOGC, \"sets runtime.GOGC\")\n\tf.IntVar(&cfg.Runtime.GOMAXPROCS, \"runtime.gomaxprocs\", defaultConfig.Runtime.GOMAXPROCS, \"sets runtime.GOMAXPROCS\")\n\tf.StringVar(&cfg.UI.Access, \"ui.access\", defaultConfig.UI.Access, \"access mode, one of [ro, rw]\")\n\tf.StringVar(&uiListenerValue, \"ui.addr\", defaultValues.UIListenerValue, \"Address the UI/API is listening on\")\n\tf.StringVar(&cfg.UI.Color, \"ui.color\", defaultConfig.UI.Color, \"background color of the UI\")\n\tf.StringVar(&cfg.UI.Title, \"ui.title\", defaultConfig.UI.Title, \"optional title for the UI\")\n\n\tf.BoolVar(&cfg.UI.RoutingTable.Source.LinkEnabled, \"ui.routingtable.source.linkenabled\", defaultConfig.UI.RoutingTable.Source.LinkEnabled, \"optional true/false flag if the source in the routing table of the admin UI should have a link\")\n\tf.BoolVar(&cfg.UI.RoutingTable.Source.NewTab, \"ui.routingtable.source.newtab\", defaultConfig.UI.RoutingTable.Source.NewTab, \"optional true/false flag if the source link should be opened in a new tab, not affected if linkenabled is false\")\n\tf.StringVar(&cfg.UI.RoutingTable.Source.Scheme, \"ui.routingtable.source.scheme\", defaultConfig.UI.RoutingTable.Source.Scheme, \"optional protocol scheme for the source link on the routing table in the admin UI, not affected if linkenabled is false\")\n\tf.StringVar(&cfg.UI.RoutingTable.Source.Host, \"ui.routingtable.source.host\", defaultConfig.UI.RoutingTable.Source.Host, \"optional host for the source link on the routing table in the admin UI, not affected if linkenabled is false\")\n\tf.StringVar(&cfg.UI.RoutingTable.Source.Port, \"ui.routingtable.source.port\", defaultConfig.UI.RoutingTable.Source.Port, \"optional port for the host of the source link on the routing table in the admin UI, not affected if linkenabled is false\")\n\n\tf.StringVar(&cfg.ProfileMode, \"profile.mode\", defaultConfig.ProfileMode, \"enable profiling mode, one of [cpu, mem, mutex, block, trace]\")\n\tf.StringVar(&cfg.ProfilePath, \"profile.path\", defaultConfig.ProfilePath, \"path to profile dump file\")\n\tf.BoolVar(&cfg.GlobMatchingDisabled, \"glob.matching.disabled\", defaultConfig.GlobMatchingDisabled, \"Disable Glob Matching on routes, one of [true, false]\")\n\tf.IntVar(&cfg.GlobCacheSize, \"glob.cache.size\", defaultConfig.GlobCacheSize, \"sets the size of the glob cache\")\n\n\tf.StringVar(&cfg.Registry.Custom.Host, \"registry.custom.host\", defaultConfig.Registry.Custom.Host, \"custom back end hostname/port\")\n\tf.StringVar(&cfg.Registry.Custom.Scheme, \"registry.custom.scheme\", defaultConfig.Registry.Custom.Scheme, \"custom back end scheme - http/https\")\n\tf.StringVar(&cfg.Registry.Custom.NoRouteHTML, \"registry.custom.noroutehtml\", defaultConfig.Registry.Custom.NoRouteHTML, \"path to file for HTML returned when no route is found\")\n\tf.BoolVar(&cfg.Registry.Custom.CheckTLSSkipVerify, \"registry.custom.checkTLSSkipVerify\", defaultConfig.Registry.Custom.CheckTLSSkipVerify, \"custom back end check TLS verification\")\n\tf.DurationVar(&cfg.Registry.Custom.Timeout, \"registry.custom.timeout\", defaultConfig.Registry.Custom.Timeout, \"timeout for API request to custom back end\")\n\tf.DurationVar(&cfg.Registry.Custom.PollInterval, \"registry.custom.pollinterval\", defaultConfig.Registry.Custom.PollInterval, \"poll interval for API request to custom back end\")\n\tf.StringVar(&cfg.Registry.Custom.Path, \"registry.custom.path\", defaultConfig.Registry.Custom.Path, \"custom back end path in the URL\")\n\tf.StringVar(&cfg.Registry.Custom.QueryParams, \"registry.custom.queryparams\", defaultConfig.Registry.Custom.QueryParams, \"custom back end query parameters in the URL\")\n\n\tf.BoolVar(&cfg.BGP.BGPEnabled, \"bgp.enabled\", defaultConfig.BGP.BGPEnabled, \"enabled bgp announcements\")\n\tf.UintVar(&cfg.BGP.Asn, \"bgp.asn\", defaultConfig.BGP.Asn, \"our BGP asn\")\n\tf.StringSliceVar(&cfg.BGP.AnycastAddresses, \"bgp.anycastaddresses\", defaultConfig.BGP.AnycastAddresses, \"comma separated list of CIDRs to broadcast - required if bgp is enabled\")\n\tf.StringVar(&cfg.BGP.RouterID, \"bgp.routerid\", defaultConfig.BGP.RouterID, \"our router ID - required if bgp is enabled\")\n\tf.IntVar(&cfg.BGP.ListenPort, \"bgp.listenport\", defaultConfig.BGP.ListenPort, \"bgp listen port.  -1 means disabled\")\n\tf.StringSliceVar(&cfg.BGP.ListenAddresses, \"bgp.listenaddresses\", defaultConfig.BGP.ListenAddresses, \"bgp listen address\")\n\tf.StringVar(&bgpPeersValue, \"bgp.peers\", defaultValues.BGPPeersValue, \"bgp peers.  comma separated list of neighboraddress=1.2.3.4;asn=65001\")\n\tf.BoolVar(&cfg.BGP.EnableGRPC, \"bgp.enablegrpc\", defaultConfig.BGP.EnableGRPC, \"enable bgp grpc listener for use with gobgp cli\")\n\tf.StringVar(&cfg.BGP.GRPCListenAddress, \"bgp.grpclistenaddress\", defaultConfig.BGP.GRPCListenAddress, \"bgp grpc cli listen address\")\n\tf.StringVar(&cfg.BGP.NextHop, \"bgp.nexthop\", defaultConfig.BGP.NextHop, \"specify the next-hop.  defaults to bgp.routerid\")\n\tf.StringVar(&cfg.BGP.GOBGPDCfgFile, \"bgp.gobgpdcfgfile\", defaultConfig.BGP.GOBGPDCfgFile, \"specify path to gobgpd config file.  overrides settings\")\n\t// deprecated flags\n\tvar proxyLogRoutes string\n\tf.StringVar(&proxyLogRoutes, \"proxy.log.routes\", \"\", \"deprecated. use log.routes.format instead\")\n\n\tvar awsApiGWCertCN string\n\tf.StringVar(&awsApiGWCertCN, \"aws.apigw.cert.cn\", \"\", \"deprecated. use caupgcn=<CN> for cert source\")\n\n\t// parse configuration\n\tif err := f.ParseFlags(cmdline[1:], environ, envprefix, props); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// post configuration\n\tif cfg.Runtime.GOMAXPROCS == -1 {\n\t\tcfg.Runtime.GOMAXPROCS = runtime.NumCPU()\n\t}\n\n\tcfg.Registry.Consul.Scheme, cfg.Registry.Consul.Addr = parseScheme(cfg.Registry.Consul.Addr)\n\n\tcertSources, err := parseCertSources(certSourcesValue)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthSchemes, err := parseAuthSchemes(authSchemesValue)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcfg.Proxy.AuthSchemes = authSchemes\n\n\tif uiListenerValue != \"\" {\n\t\tkvs, err := parseKVSlice(uiListenerValue)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(kvs) != 1 {\n\t\t\treturn nil, fmt.Errorf(\"ui.addr must contain only one listener\")\n\t\t}\n\t\tcfg.UI.Listen, err = parseListen(kvs[0], certSources, 0, 0)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcfg.Listen, err = parseListeners(listenerValue, certSources, readTimeout, writeTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cfg.Proxy.LocalIP != \"\" {\n\t\tif cfg.Proxy.LocalIP, err = gs.Parse(cfg.Proxy.LocalIP); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse local ip: %s\", err)\n\t\t}\n\t}\n\n\t// Unless registry.consul.register.addr has been set explicitly it should\n\t// be the same as ui.addr. See issue 657.\n\tif !f.IsSet(\"registry.consul.register.addr\") {\n\t\tcfg.Registry.Consul.ServiceAddr = cfg.UI.Listen.Addr\n\t}\n\tif cfg.Registry.Consul.ServiceAddr != \"\" {\n\t\tif cfg.Registry.Consul.ServiceAddr, err = gs.Parse(cfg.Registry.Consul.ServiceAddr); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to consul service address: %s\", err)\n\t\t}\n\t}\n\n\tcfg.Registry.Consul.CheckScheme = defaultConfig.Registry.Consul.CheckScheme\n\tif cfg.UI.Listen.CertSource.Name != \"\" {\n\t\tcfg.Registry.Consul.CheckScheme = \"https\"\n\t}\n\n\tif cfg.Registry.Consul.ServiceMonitors <= 0 {\n\t\tcfg.Registry.Consul.ServiceMonitors = 1\n\t}\n\n\tif gzipContentTypesValue != \"\" {\n\t\tcfg.Proxy.GZIPContentTypes, err = regexp.Compile(gzipContentTypesValue)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid expression for content types: %s\", err)\n\t\t}\n\t}\n\n\tif cfg.Proxy.Strategy != \"rr\" && cfg.Proxy.Strategy != \"rnd\" {\n\t\treturn nil, fmt.Errorf(\"invalid proxy.strategy: %s\", cfg.Proxy.Strategy)\n\t}\n\n\tif cfg.Proxy.Matcher != \"prefix\" && cfg.Proxy.Matcher != \"glob\" && cfg.Proxy.Matcher != \"iprefix\" {\n\t\treturn nil, fmt.Errorf(\"invalid proxy.matcher: %s\", cfg.Proxy.Matcher)\n\t}\n\n\tif cfg.UI.Access != \"ro\" && cfg.UI.Access != \"rw\" {\n\t\treturn nil, fmt.Errorf(\"invalid ui.access: %s\", cfg.UI.Access)\n\t}\n\n\t// go1.10 will not accept a non-three digit status code\n\tif cfg.Proxy.NoRouteStatus < 100 || cfg.Proxy.NoRouteStatus > 999 {\n\t\treturn nil, fmt.Errorf(\"proxy.noroutestatus must be between 100 and 999\")\n\t}\n\n\tif cfg.Registry.Consul.AllowStale && cfg.Registry.Consul.RequireConsistent {\n\t\treturn nil, fmt.Errorf(\"registry.consul.allowStale and registry.consul.requireConsistent cannot both be true\")\n\t}\n\n\t// handle deprecations\n\tdeprecate := func(name, msg string) {\n\t\tif f.IsSet(name) {\n\t\t\tlog.Print(\"[WARN] \", msg)\n\t\t}\n\t}\n\tdeprecate(\"proxy.log.routes\", \"proxy.log.routes has been deprecated. Please use 'log.routes.format' instead\")\n\n\tif proxyLogRoutes != \"\" {\n\t\tcfg.Log.RoutesFormat = proxyLogRoutes\n\t}\n\n\tcfg.BGP.Peers, err = parseBGPPeers(bgpPeersValue)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg, nil\n}\n\n// parseScheme splits a url into scheme and address and defaults\n// to \"http\" if no scheme was given.\nfunc parseScheme(s string) (scheme, addr string) {\n\ts = strings.ToLower(s)\n\tswitch {\n\tcase strings.HasPrefix(s, \"https://\"):\n\t\tscheme, addr = \"https\", s[len(\"https://\"):]\n\tcase strings.HasPrefix(s, \"http://\"):\n\t\tscheme, addr = \"http\", s[len(\"http://\"):]\n\tdefault:\n\t\tscheme, addr = \"http\", s\n\t}\n\n\t// strip off anything after a final slash\n\tif n := strings.Index(addr, \"/\"); n >= 0 {\n\t\taddr = addr[:n]\n\t}\n\treturn\n}\n\nfunc parseListeners(cfgs string, cs map[string]CertSource, readTimeout, writeTimeout time.Duration) (listen []Listen, err error) {\n\tkvs, err := parseKVSlice(cfgs)\n\tfor _, cfg := range kvs {\n\t\tl, err := parseListen(cfg, cs, readTimeout, writeTimeout)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlisten = append(listen, l)\n\t}\n\treturn\n}\n\nfunc parseListen(cfg map[string]string, cs map[string]CertSource, readTimeout, writeTimeout time.Duration) (l Listen, err error) {\n\tl = Listen{\n\t\tReadTimeout:  readTimeout,\n\t\tWriteTimeout: writeTimeout,\n\t}\n\n\tvar csName string\n\tfor k, v := range cfg {\n\t\tswitch k {\n\t\tcase \"\", \"addr\":\n\t\t\tif l.Addr, err = gs.Parse(v); err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\tcase \"proto\":\n\t\t\tl.Proto = v\n\t\t\tswitch l.Proto {\n\t\t\tcase \"tcp\", \"tcp+sni\", \"tcp-dynamic\", \"http\", \"https\", \"grpc\", \"grpcs\", \"https+tcp+sni\", \"prometheus\":\n\t\t\t\t// ok\n\t\t\tdefault:\n\t\t\t\treturn Listen{}, fmt.Errorf(\"unknown protocol %q\", v)\n\t\t\t}\n\t\tcase \"rt\": // read timeout\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.ReadTimeout = d\n\t\tcase \"wt\": // write timeout\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.WriteTimeout = d\n\t\tcase \"it\": // idle timeout\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.IdleTimeout = d\n\t\tcase \"cs\": // cert source\n\t\t\tcsName = v\n\t\t\tc, ok := cs[v]\n\t\t\tif !ok {\n\t\t\t\treturn Listen{}, fmt.Errorf(\"unknown certificate source %q\", v)\n\t\t\t}\n\t\t\tl.CertSource = c\n\t\t\tif l.Proto == \"\" {\n\t\t\t\tl.Proto = \"https\"\n\t\t\t}\n\t\tcase \"strictmatch\":\n\t\t\tl.StrictMatch = (v == \"true\")\n\t\tcase \"tlsmin\":\n\t\t\tn, err := parseTLSVersion(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.TLSMinVersion = n\n\t\tcase \"tlsmax\":\n\t\t\tn, err := parseTLSVersion(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.TLSMaxVersion = n\n\t\tcase \"tlsciphers\":\n\t\t\tc, err := parseTLSCiphers(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.TLSCiphers = c\n\t\tcase \"pxyproto\":\n\t\t\tl.ProxyProto = (v == \"true\")\n\t\tcase \"pxytimeout\":\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.ProxyHeaderTimeout = d\n\t\tcase \"refresh\":\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn Listen{}, err\n\t\t\t}\n\t\t\tl.Refresh = d\n\t\t}\n\t}\n\n\tif l.Proto == \"\" {\n\t\tl.Proto = \"http\"\n\t}\n\tif l.Addr == \"\" {\n\t\treturn Listen{}, fmt.Errorf(\"need listening host:port\")\n\t}\n\tif csName != \"\" && l.Proto != \"https\" && l.Proto != \"tcp\" && l.Proto != \"tcp-dynamic\" && l.Proto != \"grpcs\" && l.Proto != \"prometheus\" && l.Proto != \"https+tcp+sni\" {\n\t\treturn Listen{}, fmt.Errorf(\"cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'\")\n\t}\n\tif csName == \"\" && l.Proto == \"https\" {\n\t\treturn Listen{}, fmt.Errorf(\"proto 'https' requires cert source\")\n\t}\n\tif csName == \"\" && l.Proto == \"grpcs\" {\n\t\treturn Listen{}, fmt.Errorf(\"proto 'grpcs' requires cert source\")\n\t}\n\tif cs[csName].Type == \"vault-pki\" && !l.StrictMatch {\n\t\t// Without StrictMatch the first issued certificate is used for all\n\t\t// subsequent requests, even if the common name doesn't match.\n\t\tlog.Printf(\"[INFO] vault-pki requires strictmatch; enabling strictmatch for listener %s\", l.Addr)\n\t\tl.StrictMatch = true\n\t}\n\tif l.ProxyProto && l.ProxyHeaderTimeout == 0 {\n\t\t// We should define a safe default if proxy-protocol was enabled but no header timeout was set.\n\t\t// See https://github.com/fabiolb/fabio/issues/524 for more information.\n\t\tl.ProxyHeaderTimeout = 250 * time.Millisecond\n\t}\n\treturn\n}\n\nvar tlsver = map[string]uint16{\n\t\"tls10\": tls.VersionTLS10,\n\t\"tls11\": tls.VersionTLS11,\n\t\"tls12\": tls.VersionTLS12,\n\t\"tls13\": tls.VersionTLS13,\n}\n\nfunc parseTLSVersion(s string) (uint16, error) {\n\ts = strings.ToLower(strings.TrimSpace(s))\n\tif n, ok := tlsver[s]; ok {\n\t\treturn n, nil\n\t}\n\treturn parseUint16(s)\n}\n\nfunc parseTLSCiphers(s string) ([]uint16, error) {\n\tvar c []uint16\n\tfor _, v := range strings.Split(s, \",\") {\n\t\tv = strings.ToUpper(strings.TrimSpace(v))\n\t\tif n, ok := tlsciphers[v]; ok {\n\t\t\tc = append(c, n)\n\t\t\tcontinue\n\t\t}\n\t\tn, err := parseUint16(v)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tc = append(c, n)\n\t}\n\treturn c, nil\n}\n\nfunc parseUint16(s string) (uint16, error) {\n\tn, err := strconv.ParseUint(s, 0, 16)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint16(n), nil\n}\n\nfunc parseCertSources(cfgs string) (cs map[string]CertSource, err error) {\n\tkvs, err := parseKVSlice(cfgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcs = map[string]CertSource{}\n\tfor _, cfg := range kvs {\n\t\tsrc, err := parseCertSource(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcs[src.Name] = src\n\t}\n\treturn\n}\n\nfunc parseCertSource(cfg map[string]string) (c CertSource, err error) {\n\tif cfg == nil {\n\t\treturn CertSource{}, nil\n\t}\n\n\tc.Refresh = 3 * time.Second\n\n\tfor k, v := range cfg {\n\t\tswitch k {\n\t\tcase \"cs\":\n\t\t\tc.Name = v\n\t\tcase \"type\":\n\t\t\tc.Type = v\n\t\tcase \"cert\":\n\t\t\tc.CertPath = v\n\t\tcase \"key\":\n\t\t\tc.KeyPath = v\n\t\tcase \"clientca\":\n\t\t\tc.ClientCAPath = v\n\t\tcase \"caupgcn\":\n\t\t\tc.CAUpgradeCN = v\n\t\tcase \"refresh\":\n\t\t\td, err := time.ParseDuration(v)\n\t\t\tif err != nil {\n\t\t\t\treturn CertSource{}, err\n\t\t\t}\n\t\t\tc.Refresh = d\n\t\tcase \"vaultfetchtoken\":\n\t\t\tc.VaultFetchToken = v\n\t\tcase \"hdr\":\n\t\t\tp := strings.SplitN(v, \": \", 2)\n\t\t\tif len(p) != 2 {\n\t\t\t\treturn CertSource{}, fmt.Errorf(\"invalid header %s\", v)\n\t\t\t}\n\t\t\tif c.Header == nil {\n\t\t\t\tc.Header = http.Header{}\n\t\t\t}\n\t\t\tc.Header.Set(p[0], p[1])\n\t\t}\n\t}\n\tif c.Name == \"\" {\n\t\treturn CertSource{}, fmt.Errorf(\"missing 'cs' in %s\", cfg)\n\t}\n\tif c.CertPath == \"\" {\n\t\treturn CertSource{}, fmt.Errorf(\"missing 'cert' in %s\", cfg)\n\t}\n\tswitch c.Type {\n\tcase \"\":\n\t\treturn CertSource{}, fmt.Errorf(\"missing 'type' in %s\", cfg)\n\tcase \"file\", \"consul\":\n\t\tc.Refresh = 0\n\tcase \"path\", \"http\", \"vault\", \"vault-pki\":\n\t\t// no-op\n\tdefault:\n\t\treturn CertSource{}, fmt.Errorf(\"unknown cert source type %s\", c.Type)\n\t}\n\n\treturn\n}\n\nfunc parseAuthSchemes(cfgs string) (as map[string]AuthScheme, err error) {\n\tkvs, err := parseKVSlice(cfgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tas = map[string]AuthScheme{}\n\tfor _, cfg := range kvs {\n\t\tsrc, err := parseAuthScheme(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tas[src.Name] = src\n\t}\n\treturn\n}\n\nfunc parseAuthScheme(cfg map[string]string) (a AuthScheme, err error) {\n\tif cfg == nil {\n\t\treturn\n\t}\n\n\tfor k, v := range cfg {\n\t\tswitch k {\n\t\tcase \"name\":\n\t\t\ta.Name = v\n\t\tcase \"type\":\n\t\t\ta.Type = v\n\t\t}\n\t}\n\n\tif a.Name == \"\" {\n\t\treturn AuthScheme{}, errors.New(\"missing 'name' in auth\")\n\t}\n\n\tswitch a.Type {\n\tcase \"\":\n\t\treturn AuthScheme{}, fmt.Errorf(\"missing 'type' in auth '%s'\", a.Name)\n\tcase \"basic\":\n\t\ta.Basic = BasicAuth{\n\t\t\tFile:    cfg[\"file\"],\n\t\t\tRealm:   cfg[\"realm\"],\n\t\t\tRefresh: 0, // the htpasswd file refresh is disabled by default\n\t\t}\n\n\t\tif a.Basic.File == \"\" {\n\t\t\treturn AuthScheme{}, fmt.Errorf(\"missing 'file' in auth '%s'\", a.Name)\n\t\t}\n\t\tif a.Basic.Realm == \"\" {\n\t\t\ta.Basic.Realm = a.Name\n\t\t}\n\n\t\tif cfg[\"refresh\"] != \"\" {\n\t\t\td, err := time.ParseDuration(cfg[\"refresh\"])\n\t\t\tif err != nil {\n\t\t\t\treturn AuthScheme{}, err\n\t\t\t}\n\t\t\tif d < time.Second {\n\t\t\t\td = time.Second\n\t\t\t}\n\t\t\ta.Basic.Refresh = d\n\t\t}\n\n\tdefault:\n\t\treturn AuthScheme{}, fmt.Errorf(\"unknown auth type '%s'\", a.Type)\n\t}\n\n\treturn\n}\n\nfunc parseBGPPeers(cfgs string) ([]BGPPeer, error) {\n\tkvs, err := parseKVSlice(cfgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar peers []BGPPeer\n\tfor _, cfg := range kvs {\n\t\tpeer, err := parseBGPPeer(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpeers = append(peers, peer)\n\t}\n\treturn peers, nil\n}\n\nfunc parseBGPPeer(cfg map[string]string) (BGPPeer, error) {\n\tvar peer = *defaultBGPPeer\n\tfor k, v := range cfg {\n\t\tswitch k {\n\t\tcase \"address\":\n\t\t\tpeer.NeighborAddress = v\n\t\tcase \"port\":\n\t\t\tu, err := strconv.ParseUint(v, 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn peer, err\n\t\t\t}\n\t\t\tpeer.NeighborPort = uint(u)\n\t\tcase \"asn\":\n\t\t\tu, err := strconv.ParseUint(v, 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn peer, err\n\t\t\t}\n\t\t\tpeer.Asn = uint(u)\n\t\tcase \"multihop\":\n\t\t\tb, err := strconv.ParseBool(v)\n\t\t\tif err != nil {\n\t\t\t\treturn peer, err\n\t\t\t}\n\t\t\tpeer.MultiHop = b\n\t\tcase \"multihoplength\":\n\t\t\tu, err := strconv.ParseUint(v, 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn peer, err\n\t\t\t}\n\t\t\tpeer.MultiHopLength = uint(u)\n\t\tcase \"password\":\n\t\t\tpeer.Password = v\n\t\t}\n\n\t}\n\treturn peer, nil\n}\n"
  },
  {
    "path": "config/load_test.go",
    "content": "package config\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pascaldekloe/goe/verify\"\n)\n\nfunc TestLoad(t *testing.T) {\n\ttests := []struct {\n\t\tdesc    string\n\t\targs    []string\n\t\tenviron []string\n\t\tpath    string\n\t\tdata    string\n\t\tcfg     func(*Config) *Config\n\t\terr     error\n\t}{\n\t\t{\n\t\t\targs: []string{\"-v\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t},\n\t\t{\n\t\t\targs: []string{\"--version\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t},\n\t\t{\n\t\t\tdesc: \"-v with other args\",\n\t\t\targs: []string{\"-a\", \"-v\", \"-b\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t},\n\t\t{\n\t\t\tdesc: \"--version with other args\",\n\t\t\targs: []string{\"-a\", \"--version\", \"-b\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t},\n\t\t{\n\t\t\tdesc: \"default config\",\n\t\t\tcfg:  func(cfg *Config) *Config { return cfg },\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-insecure=true\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Insecure = true\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-profile.mode\", \"foo\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.ProfileMode = \"foo\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-profile.path\", \"foo\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.ProfilePath = \"foo\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=http\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=tcp\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"tcp\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=tcp+sni\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"tcp+sni\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=grpc\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"grpc\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=tcp-dynamic\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"tcp-dynamic\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with tls configs\",\n\t\t\targs: []string{\"-proxy.addr\", `:5555;rt=1s;wt=2s;it=3s;tlsmin=0x0300;tlsmax=0x305;tlsciphers=\"0x123,0x456\"`},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{\n\t\t\t\t\t\tAddr:          \":5555\",\n\t\t\t\t\t\tProto:         \"http\",\n\t\t\t\t\t\tReadTimeout:   1 * time.Second,\n\t\t\t\t\t\tWriteTimeout:  2 * time.Second,\n\t\t\t\t\t\tIdleTimeout:   3 * time.Second,\n\t\t\t\t\t\tTLSMinVersion: 0x300,\n\t\t\t\t\t\tTLSMaxVersion: 0x305,\n\t\t\t\t\t\tTLSCiphers:    []uint16{0x123, 0x456},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with named tls configs\",\n\t\t\targs: []string{\"-proxy.addr\", `:5555;rt=1s;wt=2s;it=3s;tlsmin=tls10;tlsmax=TLS11;tlsciphers=\"TLS_RSA_WITH_RC4_128_SHA,tls_ecdhe_ecdsa_with_aes_256_gcm_sha384\"`},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{\n\t\t\t\t\t\tAddr:          \":5555\",\n\t\t\t\t\t\tProto:         \"http\",\n\t\t\t\t\t\tReadTimeout:   1 * time.Second,\n\t\t\t\t\t\tWriteTimeout:  2 * time.Second,\n\t\t\t\t\t\tIdleTimeout:   3 * time.Second,\n\t\t\t\t\t\tTLSMinVersion: tls.VersionTLS10,\n\t\t\t\t\t\tTLSMaxVersion: tls.VersionTLS11,\n\t\t\t\t\t\tTLSCiphers:    []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with file cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name\", \"-proxy.cs\", \"cs=name;type=file;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"file\", CertPath: \"value\"}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with path cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name\", \"-proxy.cs\", \"cs=name;type=path;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"path\", CertPath: \"value\", Refresh: 3 * time.Second}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with http cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name\", \"-proxy.cs\", \"cs=name;type=http;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"http\", CertPath: \"value\", Refresh: 3 * time.Second}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with consul cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name\", \"-proxy.cs\", \"cs=name;type=consul;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"consul\", CertPath: \"value\"}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with vault cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name\", \"-proxy.cs\", \"cs=name;type=vault;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"vault\", CertPath: \"value\", Refresh: 3 * time.Second}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with vault-pki cert source\",\n\t\t\targs: []string{\n\t\t\t\t\"-proxy.addr\", \":5555;cs=name\",\n\t\t\t\t\"-proxy.cs\", \"cs=name;type=vault-pki;cert=pki/issue/value\",\n\t\t\t},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"vault-pki\", CertPath: \"pki/issue/value\", Refresh: 3 * time.Second}\n\t\t\t\tcfg.Listen[0].StrictMatch = true // implicit\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with vault-pki cert source, -proxy.cs first\",\n\t\t\targs: []string{\n\t\t\t\t\"-proxy.cs\", \"cs=name;type=vault-pki;cert=pki/issue/value\",\n\t\t\t\t\"-proxy.addr\", \":5555;cs=name\",\n\t\t\t},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"https\"}}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{Name: \"name\", Type: \"vault-pki\", CertPath: \"pki/issue/value\", Refresh: 3 * time.Second}\n\t\t\t\tcfg.Listen[0].StrictMatch = true // implicit\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name;strictmatch=true\", \"-proxy.cs\", \"cs=name;type=path;cert=foo;clientca=bar;refresh=2s;hdr=a: b;caupgcn=furb\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{\n\t\t\t\t\t\tAddr:        \":5555\",\n\t\t\t\t\t\tProto:       \"https\",\n\t\t\t\t\t\tStrictMatch: true,\n\t\t\t\t\t\tCertSource: CertSource{\n\t\t\t\t\t\t\tName:         \"name\",\n\t\t\t\t\t\t\tType:         \"path\",\n\t\t\t\t\t\t\tCertPath:     \"foo\",\n\t\t\t\t\t\t\tClientCAPath: \"bar\",\n\t\t\t\t\t\t\tRefresh:      2 * time.Second,\n\t\t\t\t\t\t\tHeader:       http.Header{\"A\": []string{\"b\"}},\n\t\t\t\t\t\t\tCAUpgradeCN:  \"furb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with cert source with full options\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name;strictmatch=true;proto=https\", \"-proxy.cs\", \"cs=name;type=path;cert=foo;clientca=bar;refresh=2s;hdr=a: b;caupgcn=furb\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{\n\t\t\t\t\t\tAddr:        \":5555\",\n\t\t\t\t\t\tProto:       \"https\",\n\t\t\t\t\t\tStrictMatch: true,\n\t\t\t\t\t\tCertSource: CertSource{\n\t\t\t\t\t\t\tName:         \"name\",\n\t\t\t\t\t\t\tType:         \"path\",\n\t\t\t\t\t\t\tCertPath:     \"foo\",\n\t\t\t\t\t\t\tClientCAPath: \"bar\",\n\t\t\t\t\t\t\tRefresh:      2 * time.Second,\n\t\t\t\t\t\t\tHeader:       http.Header{\"A\": []string{\"b\"}},\n\t\t\t\t\t\t\tCAUpgradeCN:  \"furb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.auth with source basic\",\n\t\t\targs: []string{\"-proxy.auth\", \"name=foo;type=basic;file=/some/file/on/disk;realm=realm\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.AuthSchemes = map[string]AuthScheme{\n\t\t\t\t\t\"foo\": {\n\t\t\t\t\t\tName: \"foo\",\n\t\t\t\t\t\tType: \"basic\",\n\t\t\t\t\t\tBasic: BasicAuth{\n\t\t\t\t\t\t\tFile:  \"/some/file/on/disk\",\n\t\t\t\t\t\t\tRealm: \"realm\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with prometheus and https\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name;strictmatch=true;proto=prometheus\", \"-proxy.cs\", \"cs=name;type=path;cert=foo;clientca=bar;refresh=2s;hdr=a: b;caupgcn=furb\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{\n\t\t\t\t\t\tAddr:        \":5555\",\n\t\t\t\t\t\tProto:       \"prometheus\",\n\t\t\t\t\t\tStrictMatch: true,\n\t\t\t\t\t\tCertSource: CertSource{\n\t\t\t\t\t\t\tName:         \"name\",\n\t\t\t\t\t\t\tType:         \"path\",\n\t\t\t\t\t\t\tCertPath:     \"foo\",\n\t\t\t\t\t\t\tClientCAPath: \"bar\",\n\t\t\t\t\t\t\tRefresh:      2 * time.Second,\n\t\t\t\t\t\t\tHeader:       http.Header{\"A\": []string{\"b\"}},\n\t\t\t\t\t\t\tCAUpgradeCN:  \"furb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.auth with source basic and no realm specified\",\n\t\t\targs: []string{\"-proxy.auth\", \"name=foo;type=basic;file=/some/file/on/disk\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.AuthSchemes = map[string]AuthScheme{\n\t\t\t\t\t\"foo\": {\n\t\t\t\t\t\tName: \"foo\",\n\t\t\t\t\t\tType: \"basic\",\n\t\t\t\t\t\tBasic: BasicAuth{\n\t\t\t\t\t\t\tFile:  \"/some/file/on/disk\",\n\t\t\t\t\t\t\tRealm: \"foo\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"issue 305\",\n\t\t\targs: []string{\n\t\t\t\t\"-proxy.addr\", \":443;cs=consul-cs,:80,:2375;proto=tcp+sni\",\n\t\t\t\t\"-proxy.cs\", \"cs=consul-cs;type=consul;cert=http://localhost:8500/v1/kv/ssl?token=token\",\n\t\t\t},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{\n\t\t\t\t\t{Addr: \":443\", Proto: \"https\"},\n\t\t\t\t\t{Addr: \":80\", Proto: \"http\"},\n\t\t\t\t\t{Addr: \":2375\", Proto: \"tcp+sni\"},\n\t\t\t\t}\n\t\t\t\tcfg.Listen[0].CertSource = CertSource{\n\t\t\t\t\tName:     \"consul-cs\",\n\t\t\t\t\tType:     \"consul\",\n\t\t\t\t\tCertPath: \"http://localhost:8500/v1/kv/ssl?token=token\",\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.localip\", \"1.2.3.4\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.LocalIP = \"1.2.3.4\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.strategy\", \"rnd\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.Strategy = \"rnd\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.strategy\", \"rr\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.Strategy = \"rr\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.matcher\", \"prefix\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.Matcher = \"prefix\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.matcher\", \"glob\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.Matcher = \"glob\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.matcher\", \"iprefix\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.Matcher = \"iprefix\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.noroutestatus\", \"555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.NoRouteStatus = 555\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.shutdownwait\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.ShutdownWait = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.responseheadertimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.ResponseHeaderTimeout = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.keepalivetimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.KeepAliveTimeout = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.dialtimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.DialTimeout = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.readtimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":9999\", Proto: \"http\", ReadTimeout: 5 * time.Millisecond}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.writetimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":9999\", Proto: \"http\", WriteTimeout: 5 * time.Millisecond}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.flushinterval\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.FlushInterval = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.globalflushinterval\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.GlobalFlushInterval = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.maxconn\", \"555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.MaxConn = 555\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.clientip\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.ClientIPHeader = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.tls\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.TLSHeader = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.tls.value\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.TLSHeaderValue = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.requestid\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.RequestID = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.sts.maxage\", \"31536000\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.STSHeader.MaxAge = 31536000\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.sts.subdomains\", \"true\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.STSHeader.Subdomains = true\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.header.sts.preload\", \"true\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.STSHeader.Preload = true\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.gzip.contenttype\", `^text/.*$`},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.GZIPContentTypes = regexp.MustCompile(`^text/.*$`)\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.gzip.contenttype\", \"^(text/.*|application/(javascript|json|font-woff|xml)|.*\\\\+(json|xml))(;.*)?$\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Proxy.GZIPContentTypes = regexp.MustCompile(`^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$`)\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-proxy.log.routes\", \"foobar\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Log.RoutesFormat = \"foobar\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.backend\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Backend = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.timeout\", \"5s\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Timeout = 5 * time.Second\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.retry\", \"500ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Retry = 500 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.file.path\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.File.RoutesPath = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.file.noroutehtmlpath\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.File.NoRouteHTMLPath = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.static.routes\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Static.Routes = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.static.noroutehtml\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Static.NoRouteHTML = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.addr\", \"1.2.3.4:5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Addr = \"1.2.3.4:5555\"\n\t\t\t\tcfg.Registry.Consul.Scheme = \"http\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.addr\", \"http://1.2.3.4:5555/\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Addr = \"1.2.3.4:5555\"\n\t\t\t\tcfg.Registry.Consul.Scheme = \"http\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.addr\", \"https://1.2.3.4:5555/\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Addr = \"1.2.3.4:5555\"\n\t\t\t\tcfg.Registry.Consul.Scheme = \"https\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.addr\", \"HTTPS://1.2.3.4:5555/\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Addr = \"1.2.3.4:5555\"\n\t\t\t\tcfg.Registry.Consul.Scheme = \"https\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.token\", \"some-token\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Token = \"some-token\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.kvpath\", \"/some/path\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.KVPath = \"/some/path\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.noroutehtmlpath\", \"/some/path\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.NoRouteHTMLPath = \"/some/path\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.tagprefix\", \"p-\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.TagPrefix = \"p-\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.enabled=false\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Register = false\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.namespace\", \"ns1\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.Namespace = \"ns1\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.addr\", \"1.2.3.4:5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.ServiceAddr = \"1.2.3.4:5555\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.name\", \"fab\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.ServiceName = \"fab\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.checkTLSSkipVerify=true\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.CheckTLSSkipVerify = true\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.tags\", \"a, b, c, \"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.ServiceTags = []string{\"a\", \"b\", \"c\"}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.checkInterval\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.CheckInterval = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.register.checkTimeout\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.CheckTimeout = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.service.status\", \"a, b, \"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.ServiceStatus = []string{\"a\", \"b\"}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.serviceMonitors\", \"5\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.ServiceMonitors = 5\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.host\", \"localhost:8080\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.Host = \"localhost:8080\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.scheme\", \"https\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.Scheme = \"https\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.checkTLSSkipVerify\", \"true\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.CheckTLSSkipVerify = true\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.timeout\", \"5s\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.Timeout = 5 * time.Second\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.pollinterval\", \"5s\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.PollInterval = 5 * time.Second\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.path\", \"test\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.Path = \"test\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.custom.queryparams\", \"test=1\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Custom.QueryParams = \"test=1\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-registry.consul.pollinterval\", \"5s\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Registry.Consul.PollInterval = 5 * time.Second\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-log.access.format\", \"foobar\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Log.AccessFormat = \"foobar\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-log.access.target\", \"foobar\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Log.AccessTarget = \"foobar\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-log.routes.format\", \"foobar\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Log.RoutesFormat = \"foobar\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-log.level\", \"foobar\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Log.Level = \"foobar\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.target\", \"some-target\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Target = \"some-target\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.prefix\", \"some-prefix\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Prefix = \"some-prefix\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.names\", \"some names\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Names = \"some names\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.interval\", \"5ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Interval = 5 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.timeout\", \"5s\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Timeout = 5 * time.Second\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.retry\", \"500ms\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Retry = 500 * time.Millisecond\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.graphite.addr\", \"1.2.3.4:5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.GraphiteAddr = \"1.2.3.4:5555\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.statsd.addr\", \"1.2.3.4:5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.StatsDAddr = \"1.2.3.4:5555\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.apiapp\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.APIApp = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.apikey\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.APIKey = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.apiurl\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.APIURL = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.brokerid\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.BrokerID = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.checkid\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.CheckID = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-metrics.circonus.submissionurl\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Metrics.Circonus.SubmissionURL = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-runtime.gogc\", \"555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Runtime.GOGC = 555\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-runtime.gomaxprocs\", \"555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Runtime.GOMAXPROCS = 555\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.access\", \"ro\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Access = \"ro\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.access\", \"rw\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Access = \"rw\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.addr\", \"1.2.3.4:5555\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Listen.Addr = \"1.2.3.4:5555\"\n\t\t\t\tcfg.UI.Listen.Proto = \"http\"\n\t\t\t\tcfg.Registry.Consul.ServiceAddr = \"1.2.3.4:5555\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.addr\", \":9998;cs=ui\", \"-proxy.cs\", \"cs=ui;type=file;cert=value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Listen.Addr = \":9998\"\n\t\t\t\tcfg.UI.Listen.Proto = \"https\"\n\t\t\t\tcfg.UI.Listen.CertSource.Name = \"ui\"\n\t\t\t\tcfg.UI.Listen.CertSource.Type = \"file\"\n\t\t\t\tcfg.UI.Listen.CertSource.CertPath = \"value\"\n\t\t\t\tcfg.Registry.Consul.CheckScheme = \"https\"\n\t\t\t\tcfg.Registry.Consul.ServiceAddr = \":9998\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.color\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Color = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-ui.title\", \"value\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.UI.Title = \"value\"\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"ignore aws.apigw.cert.cn\",\n\t\t\targs: []string{\"-aws.apigw.cert.cn\", \"value\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return cfg },\n\t\t},\n\n\t\t// config file\n\t\t{\n\t\t\tdesc:    \"config from environ\",\n\t\t\tenviron: []string{\"FABIO_proxy_addr=:6666\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":6666\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"config from url\",\n\t\t\targs: []string{\"-cfg\", \"URL\"},\n\t\t\tpath: \"http\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"config from file I\",\n\t\t\targs: []string{\"-cfg\", \"/tmp/fabio-config-test\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"config from file II\",\n\t\t\targs: []string{\"-cfg=/tmp/fabio-config-test\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"config from file III\",\n\t\t\targs: []string{\"-cfg='/tmp/fabio-config-test'\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"config from file IV\",\n\t\t\targs: []string{\"-cfg=\\\"/tmp/fabio-config-test\\\"\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\n\t\t// precedence rules\n\t\t{\n\t\t\tdesc: \"cmdline over config file I\",\n\t\t\targs: []string{\"-cfg\", \"/tmp/fabio-config-test\", \"-proxy.addr\", \":6666\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":6666\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"cmdline over config file II\",\n\t\t\targs: []string{\"-proxy.addr\", \":6666\", \"-cfg\", \"/tmp/fabio-config-test\"},\n\t\t\tpath: \"/tmp/fabio-config-test\",\n\t\t\tdata: \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":6666\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"environ over config file\",\n\t\t\targs:    []string{\"-cfg\", \"/tmp/fabio-config-test\"},\n\t\t\tenviron: []string{\"FABIO_proxy_addr=:6666\"},\n\t\t\tpath:    \"/tmp/fabio-config-test\",\n\t\t\tdata:    \"proxy.addr = :5555\",\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":6666\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"cmdline over environ\",\n\t\t\targs:    []string{\"-proxy.addr\", \":5555\"},\n\t\t\tenviron: []string{\"FABIO_proxy_addr=:6666\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.Listen = []Listen{{Addr: \":5555\", Proto: \"http\"}}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\n\t\t// errors\n\t\t{\n\t\t\tdesc: \"-proxy.addr with unknown cert source 'foo'\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=foo\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"unknown certificate source \\\"foo\\\"\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with unknown proto 'foo'\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=foo\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"unknown protocol \\\"foo\\\"\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with proto 'https' requires cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=https\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"proto 'https' requires cert source\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with proto 'grpcs' requires cert source\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;proto=grpcs\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"proto 'grpcs' requires cert source\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with cert source and proto 'http' requires proto 'https', 'tcp', or 'grpcs'\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name;proto=http\", \"-proxy.cs\", \"cs=name;type=path;cert=value\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.addr with cert source and proto 'tcp+sni' requires proto 'https', 'tcp' or 'grpcs'\",\n\t\t\targs: []string{\"-proxy.addr\", \":5555;cs=name;proto=tcp+sni\", \"-proxy.cs\", \"cs=name;type=path;cert=value\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.noroutestatus too small\",\n\t\t\targs: []string{\"-proxy.noroutestatus\", \"10\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"proxy.noroutestatus must be between 100 and 999\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.noroutestatus too big\",\n\t\t\targs: []string{\"-proxy.noroutestatus\", \"1000\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"proxy.noroutestatus must be between 100 and 999\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.auth with unknown auth type 'foo'\",\n\t\t\targs: []string{\"-proxy.auth\", \"name=myauth;type=foo\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"unknown auth type 'foo'\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.auth with missing name\",\n\t\t\targs: []string{\"-proxy.auth\", \"type=basic;file=/some/file;realm=realm\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"missing 'name' in auth\"),\n\t\t},\n\t\t{\n\t\t\tdesc: \"-proxy.auth basic with missing file\",\n\t\t\targs: []string{\"-proxy.auth\", \"name=foo;type=basic;realm=realm\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errors.New(\"missing 'file' in auth 'foo'\"),\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-glob.cache.size\", \"1000\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.GlobCacheSize = 1000\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-cfg\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errInvalidConfig,\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-cfg=''\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errInvalidConfig,\n\t\t},\n\t\t{\n\t\t\targs: []string{\"-cfg=\\\"\\\"\"},\n\t\t\tcfg:  func(cfg *Config) *Config { return nil },\n\t\t\terr:  errInvalidConfig,\n\t\t},\n\t\t{\n\t\t\tdesc: \"valid bgp peers\",\n\t\t\targs: []string{\"-bgp.peers\", \"address=127.0.0.3;port=1179;asn=65000;\" +\n\t\t\t\t\"multihop=true;multihoplength=5;password=hunter2\"},\n\t\t\tcfg: func(cfg *Config) *Config {\n\t\t\t\tcfg.BGP.Peers = []BGPPeer{\n\t\t\t\t\t{\n\t\t\t\t\t\tNeighborAddress: \"127.0.0.3\",\n\t\t\t\t\t\tNeighborPort:    1179,\n\t\t\t\t\t\tAsn:             65000,\n\t\t\t\t\t\tMultiHop:        true,\n\t\t\t\t\t\tMultiHopLength:  5,\n\t\t\t\t\t\tPassword:        \"hunter2\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\treturn cfg\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\tif tt.desc == \"\" {\n\t\t\ttt.desc = strings.Join(tt.args, \" \")\n\t\t}\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\t// start a web server or write data to a file if tt.path is set\n\t\t\tswitch {\n\t\t\tcase tt.path == \"http\":\n\t\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tfmt.Fprint(w, tt.data)\n\t\t\t\t}))\n\t\t\t\tdefer srv.Close()\n\n\t\t\t\t// replace 'URL' with the actual server url in the command line args\n\t\t\t\tfor i := range tt.args {\n\t\t\t\t\ttt.args[i] = strings.ReplaceAll(tt.args[i], \"URL\", srv.URL)\n\t\t\t\t}\n\n\t\t\tcase tt.path != \"\":\n\t\t\t\tif err := os.WriteFile(tt.path, []byte(tt.data), 0600); err != nil {\n\t\t\t\t\tt.Fatalf(\"error writing file: %s\", err)\n\t\t\t\t}\n\t\t\t\tdefer os.Remove(tt.path)\n\t\t\t}\n\n\t\t\t// config parser expects the exe name to be the first argument\n\t\t\tcfg, err := Load(append([]string{\"fabio\"}, tt.args...), tt.environ)\n\t\t\tif got, want := err, tt.err; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"got error %v want %v\", got, want)\n\t\t\t}\n\n\t\t\t// limit the amount of code we have to write per test:\n\t\t\t// each test has a function which augments a pre-configured\n\t\t\t// config structure which is pre-filled with the defaults.\n\t\t\tclone := new(Config)\n\t\t\t*clone = *defaultConfig\n\t\t\tclone.Listen = []Listen{{Addr: \":9999\", Proto: \"http\"}}\n\t\t\tgot, want := cfg, tt.cfg(clone)\n\t\t\tverify.Values(t, \"\", got, want)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/localip.go",
    "content": "package config\n\nimport (\n\t\"log\"\n\t\"net\"\n)\n\n// LocalIP tries to determine a non-loopback address for the local machine\nfunc LocalIP() (net.IP, error) {\n\taddrs, err := net.InterfaceAddrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, addr := range addrs {\n\t\tif ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() {\n\t\t\tif ipnet.IP.To4() != nil || ipnet.IP.To16() != nil {\n\t\t\t\treturn ipnet.IP, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc LocalIPString() string {\n\tip, err := LocalIP()\n\tif err != nil {\n\t\tlog.Print(\"[WARN] Error determining local ip address. \", err)\n\t\treturn \"\"\n\t}\n\tif ip == nil {\n\t\tlog.Print(\"[WARN] Could not determine local ip address\")\n\t\treturn \"\"\n\t}\n\treturn ip.String()\n}\n"
  },
  {
    "path": "demo/aws/.gitignore",
    "content": "*.tfstate\n*.tfstate.backup\n"
  },
  {
    "path": "demo/aws/main.tf",
    "content": "# Specify the provider and access details\nprovider \"aws\" {\n  region = \"${var.aws_region}\"\n}\n\n# Create a VPC to launch our instances into\nresource \"aws_vpc\" \"default\" {\n  cidr_block = \"10.0.0.0/16\"\n}\n\n# Create an internet gateway to give our subnet access to the outside world\nresource \"aws_internet_gateway\" \"default\" {\n  vpc_id = \"${aws_vpc.default.id}\"\n}\n\n# Grant the VPC internet access on its main route table\nresource \"aws_route\" \"internet_access\" {\n  route_table_id         = \"${aws_vpc.default.main_route_table_id}\"\n  destination_cidr_block = \"0.0.0.0/0\"\n  gateway_id             = \"${aws_internet_gateway.default.id}\"\n}\n\n# Create a subnet to launch our instances into\nresource \"aws_subnet\" \"default\" {\n  vpc_id                  = \"${aws_vpc.default.id}\"\n  cidr_block              = \"10.0.1.0/24\"\n  map_public_ip_on_launch = true\n}\n\n# A security group for the ELB so it is accessible via the web\nresource \"aws_security_group\" \"elb\" {\n  name        = \"terraform_example_elb\"\n  description = \"Used in the terraform\"\n  vpc_id      = \"${aws_vpc.default.id}\"\n\n  # HTTP access from anywhere\n  ingress {\n    from_port   = 80\n    to_port     = 80\n    protocol    = \"tcp\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  # fabio admin access from anywhere\n  ingress {\n    from_port   = 9998\n    to_port     = 9998\n    protocol    = \"tcp\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  # outbound internet access\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# Our default security group to access\n# the instances over SSH and HTTP\nresource \"aws_security_group\" \"default\" {\n  name        = \"fabio_example\"\n  description = \"Used in the terraform\"\n  vpc_id      = \"${aws_vpc.default.id}\"\n\n  # SSH access from anywhere\n  ingress {\n    from_port   = 22\n    to_port     = 22\n    protocol    = \"tcp\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  # HTTP access from the VPC\n  ingress {\n    from_port   = 9999\n    to_port     = 9999\n    protocol    = \"tcp\"\n    cidr_blocks = [\"10.0.0.0/16\"]\n  }\n\n  # HTTP access from the VPC\n  ingress {\n    from_port   = 9998\n    to_port     = 9998\n    protocol    = \"tcp\"\n    cidr_blocks = [\"10.0.0.0/16\"]\n  }\n\n  # outbound internet access\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n\nresource \"aws_elb\" \"web\" {\n  name = \"fabio-example-elb\"\n\n  subnets         = [\"${aws_subnet.default.id}\"]\n  security_groups = [\"${aws_security_group.elb.id}\"]\n  instances       = [\"${aws_instance.web.id}\"]\n\n  listener {\n    instance_port     = 9999\n    instance_protocol = \"http\"\n    lb_port           = 80\n    lb_protocol       = \"http\"\n  }\n\n  listener {\n    instance_port     = 9998\n    instance_protocol = \"http\"\n    lb_port           = 9998\n    lb_protocol       = \"http\"\n  }\n\n}\n\nresource \"aws_proxy_protocol_policy\" \"ProxyProtocol\" {\n  load_balancer = \"${aws_elb.web.name}\"\n  instance_ports = [\"9999\"]\n}\n\nresource \"aws_key_pair\" \"auth\" {\n  key_name   = \"${var.key_name}\"\n  public_key = \"${file(var.public_key_path)}\"\n}\n\nresource \"aws_instance\" \"web\" {\n  # The connection block tells our provisioner how to\n  # communicate with the resource (instance)\n  connection {\n    # The default username for our AMI\n    user = \"ubuntu\"\n\n    # The connection will use the local SSH agent for authentication.\n  }\n\n  instance_type = \"t2.micro\"\n\n  # Lookup the correct AMI based on the region\n  # we specified\n  ami = \"${lookup(var.aws_amis, var.aws_region)}\"\n\n  # The name of our SSH keypair we created above.\n  key_name = \"${aws_key_pair.auth.id}\"\n\n  # Our Security group to allow HTTP and SSH access\n  vpc_security_group_ids = [\"${aws_security_group.default.id}\"]\n\n  # We're going to launch into the same subnet as our ELB. In a production\n  # environment it's more common to have a separate private subnet for\n  # backend instances.\n  subnet_id = \"${aws_subnet.default.id}\"\n\n  # We run a remote provisioner on the instance after creating it.\n  # In this case, we just install nginx and start it. By default,\n  # this should be on port 80\n  provisioner \"remote-exec\" {\n    inline = [\n      \"sudo apt-get -y update\",\n      \"sudo apt-get -y install unzip\",\n      \"wget https://releases.hashicorp.com/consul/0.6.4/consul_0.6.4_linux_amd64.zip\",\n      \"wget https://github.com/fabiolb/fabio/releases/download/v1.1.2/fabio-1.1.2-go1.6.2_linux-amd64\",\n      \"unzip consul*.zip\"\n    ]\n  }\n}\n"
  },
  {
    "path": "demo/aws/outputs.tf",
    "content": "output \"address\" {\n  value = \"${aws_elb.web.dns_name}\"\n}\n"
  },
  {
    "path": "demo/aws/variables.tf",
    "content": "variable \"public_key_path\" {\n  default = \"~/.ssh/terraform.pub\"\n}\n\nvariable \"key_name\" {\n  default = \"terraform\"\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to launch servers.\"\n  default = \"eu-west-1\"\n}\n\n# Ubuntu Precise 14.04 LTS (x64)\nvariable \"aws_amis\" {\n  default = {\n    eu-west-1 = \"ami-f95ef58a\"\n  }\n}\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "public/\n"
  },
  {
    "path": "docs/README.md",
    "content": "# fabiolb.net website\n\nThis is the source code for the https://fabiolb.net website.\n\nIt is built with [Hugo](https://gohugo.io/) and automatically deployed to GitHub pages via\nGithub Actions.\n\nThe theme is [TheDocs](http://thetheme.io/thedocs/) from [TheTheme.io](http://thetheme.io/).\n\nThis isn't a free theme and I've paid for it. Please don't just take it. Pay the designers.\nIt isn't expensive.\n\nTo render the page locally run the following command in the `fabio` root directory:\n\n    $ hugo serve -s docs --disableFastRender\n\nTo view the site open http://localhost:1313/ in your browser.\n"
  },
  {
    "path": "docs/archetypes/default.md",
    "content": "---\ntitle: \"{{ replace .TranslationBaseName \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\n---\n"
  },
  {
    "path": "docs/config.toml",
    "content": "baseURL      = \"https://fabiolb.net/\"\nlanguageCode = \"en-us\"\ntitle        = \"fabio - The Consul Load-Balancer\"\ndisableKinds = [\"taxonomy\"]\nignoreLogs   = ['warning-goldmark-raw-html']\n\n[params]\n  googleAnalytics = \"UA-2812942-5\"\n\n"
  },
  {
    "path": "docs/content/_index.md",
    "content": "---\ntitle: \"Overview\"\n---\n\nFabio is an HTTP and TCP reverse proxy that configures itself with data from\n[Consul](https://consul.io/). \n\nTraditional load balancers and reverse proxies need to be configured with a\nconfig file. The configuration contains the hostnames and paths the proxy is\nforwarding to upstream services. This process can be automated with tools like\n[consul-template](https://github.com/hashicorp/consul-template) that generate\nconfig files and trigger a reload.\n\nFabio works differently since it updates its routing table directly from the\ndata stored in [Consul](https://consul.io/) as soon as there is a change and\nwithout restart or reloading.\n\nWhen you register a service in Consul all you need to add is a tag that\nannounces the paths the upstream service accepts, e.g. `urlprefix-/user` or\n`urlprefix-/order` and fabio will do the rest.\n\n### Maintainer\n\nFabio was developed and maintained by Frank Schröder through January, 2020.  Since that date primary maintenance\nhas been the responsibility of [ENA](https://github.com/myENA) and the great community of users.\n\nIt was originally developed at the [eBay Classifieds Group](https://www.ebayclassifiedsgroup.com/)\nin Amsterdam, The Netherlands.\n"
  },
  {
    "path": "docs/content/cfg/_index.md",
    "content": "---\ntitle: \"Config Language\"\nweight: 550\n---\n\nThe routing table is configured with commands in the language specified below.\nRules are automatically generated by the configured backend. Additional rules\ncan be stored in the Consul KV store configured by\n[`registry.consul.kvpath`](/ref/registry.consul.kvpath/) which by default is\n`/fabio/config`. \n\nAs of fabio 1.5.7 the path is interpreted as a prefix and the values of all sub\nkeys are appended in alphabetical order. When the\n[`log.routes.format`](/ref/log.routes.format/) is set to `all` then the routing\ntable contains comments on the source of the fragment.\n\n### Comments\n\nBlank lines and lines starting with `#` or `//` are ignored.\n\n### `route add`\n\nAdd a route for a service `svc` for the `src` (e.g. `/path` or `:port`) to a `dst` (e.g. `URL` or `host:port`).\n\n`route add <svc> <src> <dst>[ weight <w>][ tags \"<t1>,<t2>,...\"][ opts \"k1=v1 k2=v2 ...\"]`\n\nOption                                     | Description\n------------------------------------------ | -----------\n`allow=ip:10.0.0.0/8,ip:fe80::/10`         | Restrict access to source addresses within the `10.0.0.0/8` or `fe80::/10` CIDR mask.  All other requests will be denied.\n`deny=ip:10.0.0.0/8,ip:fe80::1234`         | Deny requests that source from the `10.0.0.0/8` CIDR mask or `fe80::1234`.  All other requests will be allowed.\n`strip=/path`                              | Forward `/path/to/file` as `/to/file`\n`prepend=/prefix`                          | Forward `/path/to/file` as `/prefix/path/to/file`\n`proto=tcp`                                | Upstream service is TCP, `dst` must be `:port`\n`pxyproto=true`                            | Enables PROXY protocol on outbount TCP connection\n`proto=https`                              | Upstream service is HTTPS\n`tlsskipverify=true`                       | Disable TLS cert validation for HTTPS upstream\n`host=name`                                | Set the `Host` header to `name`. If `name == 'dst'` then the `Host` header will be set to the registered upstream host name\n`register=name`                            | Register fabio as new service `name`. Useful for registering hostnames for host specific routes.\n`auth=name`                                | Specify an auth scheme to use (must be registered with the fabio server using `proxy.auth`)\n\n##### Example\n\n```\n# route traffic for product-svc to 1.2.3.4:8000 and :9000\nroute add product-svc /product http://1.2.3.4:8000\nroute add product-svc /product http://1.2.3.4:9000\n```\n\n### `route del`\n\nRemove one or more routes which match the given criteria.\n\n```\nroute del <svc>[ <src>[ <dst>]]\nroute del <svc> tags \"<t1>,<t2>,...\"\nroute del tags \"<t1>,<t2>,...\"\n```\n\n##### Example\n\n```\n# remove all routes for 'product-svc'\nroute del product-svc\n\n# remove all routes for 'product-svc' and /path\nroute del product-svc /path\n\n# remove single route\nroute del product-svc /path http://1.2.3.4:8000\n\n# remove all routes for 'product-svc' matching a tag\nroute del product-svc tags \"green\"\n\n# remove all routes matching a tag\nroute del tags \"yesterday\"\n```\n\n### `route weight`\n\nDirects a certain amount of traffic to instances matching certain criteria.\n\nThe weight `w` is expressed as follows:\n\n * `w` is a float > 0 describing a percentage, e.g. 0.5 == 50%\n * `w <= 0`: means no fixed weighting. Traffic is evenly distributed\n * `w > 0`: route will receive n% of traffic. If sum(w) > 1 then w is normalized.\n * `sum(w) >= 1`: only matching services will receive traffic\n\nNote that the total sum of traffic sent to all matching routes is `w`%.\n\nThe order of commands matters but routes are always ordered from most to least\nspecific by prefix length.\n\n```\nroute weight <svc> <src> weight <w> tags \"<t1>,<t2>,...\"\nroute weight <src> weight <w> tags \"<t1>,<t2>,...\"\nroute weight <svc> <src> weight <w>\nroute weight service host/path weight w tags \"tag1,tag2\"\n```\n\n##### Example\n\n```\n# Route 5% of the traffic to product-svc:/path with tags \"green\"\nroute weight product-svc /path weight .05 tags \"green\"\n\n# Route 5% of the traffic to '/path' to the Go implementation.\n# Route the rest (95%) to the other implementation\nroute weight /path weight .05 tags \"lang=go\"\n\n# Route 5% of the traffice to '/path' on the product-svc-go\nroute weight product-svc-go /path weight .05\n```\n\n### Routing rules\n\nThe routing table contains first all routes with a host sorted by prefix\nlength in descending order and then all routes without a host again sorted by\nprefix length in descending order.\n\nFor each incoming request the routing table is searched top to bottom for a\nmatching route. A route matches if either `host/path` or - if there was no\nmatch - just `/path` matches.\n\nThe matching route determines the target URL depending on the configured\nstrategy. `rnd` and `rr` are available with `rnd` being the default.\n\n##### Example\n\nThe auto-generated routing table is\n\n```\nroute add service-a www.mp.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-a www.kjca.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-a www.dba.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-b www.mp.dev/auth/ http://host-b:11080/ tags \"a,b\"\nroute add service-b www.kjca.dev/auth/ http://host-b:11080/ tags \"a,b\"\nroute add service-b www.dba.dev/auth/ http://host-b:11080/ tags \"a,b\"\n```\n\nThe manual configuration under `/fabio/config` is\n\n```\nroute del service-b www.dba.dev/auth/\nroute add service-c www.somedomain.com/ http://host-z:12345/\n```\n\nThe complete routing table then is\n\n```\nroute add service-a www.mp.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-a www.kjca.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-a www.dba.dev/accounts/ http://host-a:11050/ tags \"a,b\"\nroute add service-b www.mp.dev/auth/ http://host-b:11080/ tags \"a,b\"\nroute add service-b www.kjca.dev/auth/ http://host-b:11080/ tags \"a,b\"\nroute add service-c www.somedomain.com/ http://host-z:12345/\n```\n"
  },
  {
    "path": "docs/content/code-of-conduct/_index.md",
    "content": "---\ntitle: \"Code of Conduct\"\nweight: 1000\n---\n\nBe nice and treat others with respect.\n\nPlease contact a project owner if you need mediation with a dispute.\n"
  },
  {
    "path": "docs/content/contact/_index.md",
    "content": "---\ntitle: \"Contact\"\nweight: 2000\n---\n\nPlease create a [GitHub](https://github.com/fabiolb/fabio/issues) issue for all feature requests, bugs and general\nquestions about the project.  You may also contact any project owner for other concerns.\n"
  },
  {
    "path": "docs/content/contrib/_index.md",
    "content": "---\ntitle: \"Contributing\"\nweight: 900\n---\n\nContributions to fabio of any kind are welcome including documentation, examples,\nfeature requests, bug reports, discussions, helping with issues, etc.\n\nIf you have a question on how or what to contribute just open an issue and\nindicate that it is a question.\n"
  },
  {
    "path": "docs/content/contrib/development.md",
    "content": "---\ntitle: \"Development\"\nweight: 200\n---\n\nFor newcomers to Go, you can't just `git clone` your forked repo and work from\nthere, due to how Go's `GOPATH` works. You can follow the steps below to get started:\n\n1. Fork this repository to your own account (named `myfork` below)\n1. Make sure you have [Consul](https://www.consul.io/downloads.html) and [Vault](https://www.vaultproject.io/downloads.html) installed in your `$PATH`\n1. `go get github.com/fabiolb/fabio`, change to the directory where the code was cloned\n   (`$GOPATH/src/github.com/fabiolb/fabio`) and add your fork as remote: `git remote add myfork git@github.com:myfork/fabio.git`\n1. Hack away!\n1. `go fmt` and `make test` your code\n1. Commit your changes and *push to your own fork*: `git push myfork`\n1. Create a pull-request\n"
  },
  {
    "path": "docs/content/contrib/guidelines.md",
    "content": "---\ntitle: \"Guidelines\"\nweight: 100\n---\n\n### Your contribution is welcome!\n\nTo make merging code as seamless as possible\nwe ask for the following:\n\n* For small changes and bug fixes go ahead, fork the project, make your changes\n  and send a pull request. Check out the [Development](/contrib/development/)\n  page for some useful tips.\n* Larger changes should start with a proposal in an issue. This should ensure\n  that the requested change is in line with the project and similar work is not\n  already underway.\n* Only add libraries if they provide significant value. Consider copying the code\n  (attribution) or writing it yourself.\n* Manage dependencies with `go mod` and run `go mod vendor` afterwards to\n  sync the `vendor` folder for backwards compatibility.\n\nOnce you are ready to send in a pull request, be sure to:\n\n* Sign the [CLA](https://cla-assistant.io/fabiolb/fabio)\n* Provide test cases for the critical code which test correctness. If your code\n  is in a performance critical path make sure you have performed some real world\n  measurements to ensure that performance is not degregated.\n* `go fmt` and `make test` your code\n* Squash your change into a single commit with the exception of additional libraries.\n* Write a good commit message.\n"
  },
  {
    "path": "docs/content/deploy/_index.md",
    "content": "---\ntitle: \"Deployment\"\nweight: 300\n---\n\nThe main use-case for fabio is to distribute incoming HTTP(S) and TCP requests\nfrom the internet to frontend (FE) services which can handle these requests.\nIn this scenario the FE services then use the service discovery feature in\n[Consul](https://consul.io/) to find backend (BE) services they need in order\nto serve the request.\n\nThat means that fabio is currently not used as an FE-BE or BE-BE router to\nroute traffic among the services themselves since the service discovery of\n[Consul](https://consul.io/) already solves that problem. Having said that,\nthere is nothing that inherently prevents fabio from being used that way.\nIt just means that we are not doing it.\n\n"
  },
  {
    "path": "docs/content/deploy/amazon-api-gw.md",
    "content": "---\ntitle: \"Amazon API Gateway\"\nweight: 500\n---\n\nYou can deploy fabio as the target of an [Amazon API Gateway](https://aws.amazon.com/api-gateway/).\n\n<pre>\ninternet -- HTTP/HTTPS --> API GW -+- HTTP -> fabio -+-> service-b (host-b)\n</pre>\n\nor behind an ELB with PROXY protocol support:\n\n<pre>\n                                           +- HTTP w/PROXY -> fabio -+-> service-a (host-a)\n                                           |                         |\ninternet -- HTTP/HTTPS --> API GW --> ELB -+- HTTP w/PROXY -> fabio -+-> service-b (host-b)\n                                           |                         |\n                                           +- HTTP w/PROXY -> fabio -+-> service-c (host-c)\n</pre>\n\nYou can authenticate calls from the API Gateway with a client certificate. This requires that you\nconfigure an HTTPS listener on fabio with a valid certificate.\n\n<pre>\ninternet -- HTTPS --> API GW -+- HTTPS w/client cert -> fabio -+-> service\n</pre>\n\nTo enable fabio to validate the Amazon\ngenerated certificate you need to configure the `aws.apigw.cert.cn` as follows:\n\n    proxy.addr = 1.2.3.4:9999;your/cert.pem;your/key.pem;api-gw-cert.pem\n    aws.apigw.cert.cn = ApiGateway\n\n`api-gw-cert.pem` is the certificate generated in the AWS Management Console. `your/cert.pem` and `your/key.pem`\nis the certificate/key pair for the HTTPS certificate. Since the Amazon API Gateway certificates don't have the `CA` flag set fabio needs to trust them for the client certificate authentication to work. Otherwise, you will get an `TLS handshake error: failed to verify client's certificate`. See [Issue 108](https://github.com/fabiolb/fabio/issues/108) for details.\n\n**Note:** The `aws.apigw.cert.cn` parameter will not be supported in version 1.2 and later which support dynamic certificate stores. You will have to add the `caupgcn=ApiGateway` parameter to the certificate source configuration instead. See [Certificate Stores](/feature/certificate-stores/) for more detail.\n"
  },
  {
    "path": "docs/content/deploy/amazon-elb.md",
    "content": "---\ntitle: \"Amazon ELB\"\nweight: 400\n---\n\nYou can deploy fabio behind an Amazon ELB and enable [PROXY protocol support](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/enable-proxy-protocol.html) to get the remote address and port of the client.\n\n<pre>\n                                +- HTTP w/PROXY proto -> fabio -+-> service-a (host-a)\n                                |                               |\ninternet -- HTTP/HTTPS --> ELB -+- HTTP w/PROXY proto -> fabio -+-> service-b (host-b)\n                                |                               |\n                                +- HTTP w/PROXY proto -> fabio -+-> service-c (host-c)\n</pre>\n"
  },
  {
    "path": "docs/content/deploy/direct.md",
    "content": "---\ntitle: \"Direct\"\nweight: 100\n---\n\nIn the following setup fabio is configured to listen on the public ip(s)\nwhere it can optionally terminate SSL traffic for one or more domains - one ip per domain.\n\n<pre>\n                                           +--> service-a\n                                           |\ninternet -- HTTP/HTTPS --> fabio -- HTTP --+--> service-b\n                                           |\n                                           +--> service-c\n</pre>\n\nTo scale fabio you can deploy it together with the frontend services which provides\nhigh-availability and distributes the network bandwidth.\n\n<pre>\n           +- HTTP/HTTPS -> fabio -+- HTTP -> service-a (host-a)\n           |                       |\ninternet --+- HTTP/HTTPS -> fabio -+- HTTP -> service-b (host-b)\n           |                       |\n           +- HTTP/HTTPS -> fabio -+- HTTP -> service-c (host-c)\n</pre>\n"
  },
  {
    "path": "docs/content/deploy/existing-lb.md",
    "content": "---\ntitle: \"Behind existing Gateway\"\nweight: 200\n---\n\nIn the following setup fabio is configured receive all incoming traffic\nfrom an existing gateway which also terminates SSL for one or more domains.\n\n<pre>\n                                                          +--> service-a\n                                                          |\ninternet -- HTTP/HTTPS --> LB -- HTTP --> fabio -- HTTP --+--> service-b\n                                                          |\n                                                          +--> service-c\n</pre>\n\nAgain, to scale fabio you can deploy it together with the frontend services\nwhich provides high-availability and distributes the network bandwidth.\n\n<pre>\n                               +- HTTP -> fabio -+-> service-a (host-a)\n                               |                 |\ninternet -- HTTP/HTTPS --> LB -+- HTTP -> fabio -+-> service-b (host-b)\n                               |                 |\n                               +- HTTP -> fabio -+-> service-c (host-c)\n</pre>\n"
  },
  {
    "path": "docs/content/faq/_index.md",
    "content": "---\ntitle: \"FAQ\"\nweight: 800\n---\n"
  },
  {
    "path": "docs/content/faq/binding-to-low-ports.md",
    "content": "---\ntitle: \"Binding to Low Ports\"\n---\n\nIf you want to bind fabio to ports below 1024 - so called privileged ports -\nwithout running fabio as `root` you can use an operating system approach as\ndescribed below.\n\nThese best practices are taken from https://github.com/fabiolb/fabio/issues/195.\n\n#### Linux\n\nProvide `net_bind_service` capability to fabio binary\n\n```\n$ setcap 'cap_net_bind_service=+ep' $(which fabio)\n```\n\nWhen using `systemd` you can use the following service definition:\n\n```\n$ cat /etc/systemd/system/fabio.service\n\n[Unit]\nDescription=Fabio proxy\nAfter=syslog.target\nAfter=network.target\n\n[Service]\nLimitMEMLOCK=infinity\nLimitNOFILE=65535\nType=simple\n# unprivileged uid and gid\nUser=fabio_user\nGroup=fabio_group\nWorkingDirectory=/\nExecStart=/path/to/fabio -cfg /path/to/fabio.conf\nRestart=always\n# no need that fabio messes with /dev\nPrivateDevices=yes\n# dedicated /tmp\nPrivateTmp=yes\n# make /usr, /boot, /etc read only\nProtectSystem=full\n# /home is not accessible at all\nProtectHome=yes\n# to be able to bind port < 1024\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nNoNewPrivileges=yes\n# only ipv4, ipv6, unix socket and netlink networking is possible\n# netlink is necessary so that fabio can list available IPs on startup\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK\n```\n\n#### Solaris/Illumos/SmartOS\n\nProvide `net_privaddr` privileges to fabio user\n\n```shell\n$ /usr/sbin/usermod -K defaultpriv=basic,net_privaddr fabio_user\n\n$ grep fabio_user /etc/user_attr\nfabio_user::::type=normal;defaultpriv=basic,net_privaddr\n```\n\n#### Provide privilege to fabio process (syntax needs review)\n\n```shell\n$ /usr/sbin/ppriv -s PELI+NET_PRIVADDR -e fabio\n```\n\n#### OpenBSD/FreeBSD/NetBSD\n\nUse `PF` to forward from low port to high port.\n\n```\n/etc/pf.conf\n\nEXT_IF = \"eth0\"\nHTTPS_PORT = 443\nHTTPS_PORT_BACKEND = 4343\nLOCAL_IP = \"127.0.0.1\"\n\n...\n\npass in quick on $EXT_IF inet proto tcp from any to $LOCAL_IP port $HTTPS_PORT rdr-to $LOCAL_IP port $HTTPS_PORT_BACKEND\n\n```\n\n#### FreeBSD: Change the range of reserved ports (this looks dangerous)\n\n```shell\n$ sysctl net.inet.ip.portrange.reservedhigh=79\n\n# add to /etc/sysctl.conf to make this permament\n```\n\n#### macOS (needs review by SME)\n\nUse `launchd` to launch fabio by creating a service plist and using launchctl to run it:\n\n`$sudo launchctl load -w /path/to/fabio.plist`\n\nExample plist XML (needs reviewing):\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist\n  PUBLIC '-//Apple//DTD PLIST 1.0//EN'\n  'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>Label</key>\n\t\t<string>com.github.fabiolb.fabio</string>\n\t\t<key>Program</key>\n\t\t<string>fabio</string>\n\t\t<key>Sockets</key>\n\t\t<dict>\n\t\t\t<key>Listeners</key>\n\t\t\t<dict>\n\t\t\t\t<key>SockServiceName</key>\n\t\t\t\t<string>80</string>\n\t\t\t\t<key>SockType</key>\n\t\t\t\t<string>stream</string>\n\t\t\t\t<key>SockFamily</key>\n\t\t\t\t<string>IPv4</string>\n\t\t\t</dict>\n\t\t</dict>\n\t</dict>\n</plist>\n```\n\n#### Windows\n\n???\n"
  },
  {
    "path": "docs/content/faq/multiple-protocol-listeners.md",
    "content": "---\ntitle: \"Handling Multiple Protocols\"\n---\n\nIt is quite possible for a single fabio instance to serve multiple protocols \nvia distinct listeners.\n\nIn this example:\n```\nproxy.addr = 172.16.20.11:80;proto=http;rt=60s;wt=30s,\\\n             172.16.20.11:443;proto=https;rt=60s;wt=30s;cs=all;tlsmin=10, \\\n             172.16.20.11:8443;proto=tcp+sni\n```\n\nWe are telling fabio to bind to `172.16.20.11` on three different ports \n(`80`, `443`, and `8443`) using three distinct protocols \n(`HTTP`, `HTTPS`, `TCP+SNI`).  You are free to bind to as many address, \nport, and protocol combinations as needed within a single instance.\n\nSee [#490](https://github.com/fabiolb/fabio/issues/490) for context.\n"
  },
  {
    "path": "docs/content/faq/request-debugging.md",
    "content": "---\ntitle: \"Test fabio with curl\"\n---\n\n##### How do I send a request to fabio via `curl`?\n\n```\ncurl -v -H 'Host: foo.com' 'http://localhost:9999/path'\n```\n\nThe `-x` or `--proxy` options will most likely not work as you expect as they\nsend the full URL instead of just the request URI which usually does not match\nany route but the default one - if configured.\n"
  },
  {
    "path": "docs/content/faq/verifying-releases.md",
    "content": "---\ntitle: \"Verifying Releases\"\n---\n\nfabio releases can be verified by comparing the SHA256 checksum\nand by verifying the checksums with a GPG key.\n\nYou can verify the SHA256 checksums with the GPG key below.\nYou can also download it from most key servers using the ID\n\nFor fabio release 5.14 and newer:\n\n[`76462AB9B0C185ABC66FD98F59861FC4870361CA`](https://keyserver.ubuntu.com/pks/lookup?search=76462AB9B0C185ABC66FD98F59861FC4870361CA&fingerprint=on&op=index)\n\n    -----BEGIN PGP PUBLIC KEY BLOCK-----\n\n    mQINBF40mfQBEADHOlocoiOY66SLZtzJjCNKFeerYH2zHNU3sLK+sHp/76MUrPV4\n    uDG3T6a6QK0HUKLy/hxKh/wftNCOaSYTwNVbYJ1EYBnBEgxuKNM8K5xOCKjwWrXF\n    J80xoXBJXXmJvOFHEoWjUnDAMVUJyf3bt0sT0vOA5OTdbd2LhimDOpeIiO/umZKp\n    0ZsDcjUPUuIenqnKyk4UwAfXdWxrj2g5/But1n3nasvgtEtQg9CaSloh6Zgzcy+3\n    I+jpCn2FLOay+THABkM+XmjSYudkIFlqsZwkB2GxwTaRXENt8QUK7i4GWVCcPN6x\n    gYgIz9uLZQXkkxGZvasC5fUm/W6F0pyz1wUbbizhDuBhoez3XdJdhW8nWCT6rg5M\n    ejgkSVoG/fqoG9SoFXeTlQjZJSc8+0pTgWsqnuwmM+eFllvORSKS7uwBNg7jvFPv\n    4yLGCR5bGxTX7VM4XPkLR2pUF/nHmSohiGOWpqw+PRVwOWBBMi+r4c4SckR4MMOB\n    NK+KJTQnildsnqw/mvf98Op4GrAtD4MQDFRKD2TSIq60qFTe6MF77P50Z33r6x5x\n    CPN7XYTzZKPPiHf5uWtyOvH3V+vxHX2N0zAsRADXW+Jsly/Wwt0k8Km3beaF/Jvw\n    AFQwneh50L5Pv+Tb+8b6xS8gvIeGPgs4lcxRDxFEcFKN58OjtDelN212cQARAQAB\n    tCVhZG1pbkBmYWJpb2xiLm5ldCA8YWRtaW5AZmFiaW9sYi5uZXQ+iQJUBBMBCgA+\n    FiEEdkYqubDBhavGb9mPWYYfxIcDYcoFAl40mfQCGwMFCQk/xgAFCwkIBwMFFQoJ\n    CAsFFgIDAQACHgECF4AACgkQWYYfxIcDYcqCgQ/8CfH2EBmBlHB7jlI4nFu17fqV\n    WTXxhuo2UcTCQ3G8at32V27FZTFq64rtY7/QmY3HyHhdn77NXzIlLDsaD07IEBpw\n    GFf05V1vVm/Y+DB/3vmHr+bEP5bB4RZqYz+U1cSGTEg2S3sOuz416gJdoCFN8Lin\n    1fHRuGfZTJ2j2oQhUsYbt+GBpPm7xtpqK4yfCd4gT2vhDzbDG9QSLMrrLh/aA6Ya\n    IcZZCsXpnRPhfvPrp0LuIY9Lml+EaMfNxsoXYl2W5c+BpXG93ThSLKPc8XM/7e4A\n    CkRWNLKihVZNDmGCIy2FIFIV9YlEIhAAtZPhsUE3rnrIUgHETPYwDvAJB4pbJrLe\n    bwnRuWZlYNsPZp8W4RxbQVcHpsg+sWoyAkykWxs9FbxgXEGd0+wP5tumFquyfijg\n    eQLnsFU7KlQA+5Rh6ulrvzMNHFBYLoPa+U1soR6Jg0hCPhkzc+6tzTrmUCg7H7+i\n    49szuN2KZr6k5GR+f2p9mOlnHmjJSJVULtnBQJMfTEnqzszvw9OgO1j72x7hTVRO\n    UQSV6NXr0GFr293iTJS1x2/zFETCZelxVwbyp0t/psDz8nv6aXMcSjzcoWgmRRcP\n    zpfNidLLp3Ym9XKtz7kvPI/PRTsHoO+qw6H6Kw8jMxxIv5hApCI/YOt5GFBlXmZq\n    hBckyt1rS0kW5zQsStS5Ag0EXjSZ9AEQAMrim1LXqnqdMJlc6sj++TZgoLeYmtSI\n    4n/J1AGk9/BIumJKgCL5TPvUhz7HUWjhOqhtH/1/EyxPTI25Up7QcQKb0TYG/6Gn\n    3mIeBsvTdPZWmwq0e7aCrTSU8bYNnuMKAFxlPPG/lu7v1QQkaPgbEMOZI7cDA7V8\n    TLs/uQcAjGPdu2f2mJ/m+kgjeOwud+43CF4aI2/eVd39DqjjDrRImUc3OXypE4vW\n    PRq2ooSnS7VE0yU3QBubdPB8Y7x7R5bDE9fgLjZ9t//bSLgZfVzZoc7TvycH9opk\n    zr1LD4XEdZYFWc1h7++ci+f75/QQppPto3ItK61oUnpyO5J0Bl/Ay7086xU8b5Be\n    mPFDVMUE8SW2a+baaDKwbYUvImSI2CwNkCuYieGuAueMkY+Coe7AdaDhtuzINkby\n    e9ALGGbpRi/ByURQoW9akQt+ap7I8/bdp+IFYWT8K1HFogd5y0+TYaatpnT9jJYM\n    64GtnDhyD2ncyLNM1a7YOn4e+WWiK8datzn962VsaSXjAPKvVROkgLoedDU9oiDm\n    ITDZgcsyY6ATgYmzlN2Qm8ubig1adZdGWsWzv0d9Qj8AEzsPqRVrQ6Ofc/sNi5Y3\n    ELSOpWUOetbKEBFYe3oA2Bu6LOqd3lcKittWke3RMkehKFqxFdmBwjcrCtIjLicv\n    IemWK6rAAYmJABEBAAGJAjwEGAEKACYWIQR2Riq5sMGFq8Zv2Y9Zhh/EhwNhygUC\n    XjSZ9AIbDAUJCT/GAAAKCRBZhh/EhwNhyrzoEACe9SVpr6TaFvIcfcvj9d4FOmiK\n    Tgm64SEnYDDs6JhzD3p38Ut80d6y2vg9WUMUA3dhftbAyr/rqkZghiV3UhWJGPJm\n    AGWVG3p5TpSPCloFUlHHMWXCJm4UAoo75ud15PYD8CtUfOYc68A7a+9f+1dC5gRy\n    rVjBltWshsai+CjksRlg64wGMvJL7ghcsGoxFOzU/khGvo5JZ3OzObscYLxBKPnY\n    sUPerHnKB63CYxNfkd2aziapE7zXqoN1ZAFKwsBp38CiuBIT+8bb6+vAy9azfW/J\n    mGqjn4vfBUpdTsPbRRRI3CAoUN8R5QqVCCzV6hcv2p921ZWNpO0QxaHJYq0W3mwH\n    ls5eJOWJwx3qZ8ZB84fnuUb1YhzNjOSJDjgE8ZJ1iHf+ZTpqNRNbsyshfPcI5FYR\n    /PKPXTGNTeTFAXiQ/UjxFK/UEVWs3mDfqtyvC+Z5s7jCGabPwoOvWeHGMHUWWZRv\n    NU+TL+pUMWY29wKsDsk7zriokCDApNnJJb52/tIzk/XHMLPBjGSoYinKYMALYbAp\n    6UvSeJ6cJ/+5vwXJadMyiYrsPPQiuVCUfVg6KcX6B/+2MaKoyY3s8DaZ1vFdtZcg\n    1tjLI383GOEuDGfUDOgrlTikgpxbT2q4Zq80aQhPD8mMlpqdTO4UWfvwwx0FPH04\n    5xVKlvTztaHhtaWHkg==\n    =b3Un\n    -----END PGP PUBLIC KEY BLOCK-----\n\n\nFor release 5.13 and older:\n\n[`D8B19A29317E92E470D7CD67021E03CADDA53977`](http://pgp.key-server.io/search/0xD8B19A29317E92E470D7CD67021E03CADDA53977)\n\n\n\t-----BEGIN PGP PUBLIC KEY BLOCK-----\n\tVersion: GnuPG v2\n\n\tmQENBFHXufMBCADO35ztkc+e22Oyfxa7npmqljgZs4O3qFB3YBY0AiFqZ+YDwc1P\n\t2sb9r76M6J9sMiijFHZ4NZkHm1NOPgiEK13fLc/cDlrDMbbv7yqrBlYZuaQxPvCw\n\tBv+zAyVyNqy79sbQpXId7bokAMthrAf69x9F1/HaBmqspi6/8JWcQmNcGVqaABRk\n\teQSB/Oq8DYBawroMRUGNtyTMKJ5FAbsYeDH7kiOlBtJxaxdhzlMX/4W6PUVXCOF+\n\t44CKVWl7eIwXkdbkAVOy2AgqG6b+X9svbjNvV0GFErozHwCjIxSKT2m/jTkey4oq\n\tst9eBuNClEKtduxjCzkbhLX+Xvqg9vPNCY1NABEBAAG0K0ZyYW5rIFNjaHJvZWRl\n\tciA8ZnJhbmsuc2Nocm9lZGVyQGdtYWlsLmNvbT6JAT4EEwECACgCGy8GCwkIBwMC\n\tBhUIAgkKCwQWAgMBAh4BAheABQJX1Z3mBQkJwErzAAoJEAIeA8rdpTl3MucIAIqx\n\t0qPeNCiT0EnJfMNaI0ttx/+Y+hF/35XqXbuhAXDPUSwqyNAt+6qdKwnc7J4ZVZx6\n\trdH0jUoNbXoN/y/QUsmtktiQqmnyFaAT3CUphg5ZcB6g+/RUPJ0uyXY+UgB7LhLd\n\ttyYyxJamfhpf0O+IEVQ+MqTvI6glCoN0s7LGGJR+/E5xbrJv8VdGrHFSPe6i4nU9\n\taxz38MzEkHPDYUcd+6QaYN82tiuL+ipkHudOOs4aO02x18g6cg7BBZFKrAPLP7SX\n\tTTG94mRhf9OEeKc/gTrHqQ+ZBrwyDZKS13LHoHYLkRVIyWDl3t3SU/U+TGsroR4a\n\tdGQoe4tJPzZ3X5hAlK20JkZyYW5rIFNjaHJvZWRlciA8ZnJzY2hyb2VkZXJAZWJh\n\teS5jb20+iQE+BBMBAgAoAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCV9Wd\n\t5gUJCcBK8wAKCRACHgPK3aU5d5hFCACe+JXpMRfqsfvc8rc4euxg0GSQGI8XfBx+\n\tqziCO37cPTtAxyQGg5Z/ZXe6xv1kDP73mUkpZ9DLBlbCdYtC/l1LG+cYV8f/sa9K\n\tFTn924j86R5ABqwgyo2ACE5iDFOA52ud2ZqVrjjOfqzShQZGanM+X+9A+5NHO7ZD\n\tRG2LqR+b9VG8bKIhbCddu6q0/CB722PSqCVo4tAZ4W6oiA2D6QB/GfMUswSntN7e\n\tnhyjEWM6701Kk5hcTrsAIEMPRLwz+NwEb63cJ5XNsIl6vIsBkGtuxTSz/2/ecwlp\n\thh4XWLTG+I+AkEo4mUUCMdieRf+IGjXXnogmJyfGtE2BTraO+v48tC1GcmFuayBT\n\tY2hyb2VkZXIgPGZyYW5rLnNjaHJvZWRlckBnby1sZWZ0LmNvbT6JAT4EEwECACgC\n\tGy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheABQJX1Z3mBQkJwErzAAoJEAIeA8rd\n\tpTl3qqkH/2Jmm44cZNDAmleoOq/gOBdDUE1Ot8wiNOnaa08Eem9DyRJ312TDXUTu\n\tgyCyNFfimln4G+XYRqw6r1Vxxtv7VMZmAaVd2q6aAC+SEbAYo32mNU5cVF1FTu04\n\tVTUlkV5jkG1mHBOVs3yxkxd+YPZQsXD3GaaFhd6/NCpqrAxzkGcEZvoHWz8vGTlf\n\tRxX5qnQeLg8VxttjSuXpfHbYptAwYmX+nDC6lL9IriJhRFtH3dVed2iHIiYtxlF6\n\t7enUv6PndL1GHHXNzl5CFtJaG2Tr8NgdCTycymLAonXvm8j4zE1jRihnZ4TS+6Zk\n\tUYS6G01WlBeHLEvCCxt8vpzKQFgg0kS5AQ0EUde58wEIAMgdQBVvrzrIJmN6i5B0\n\t6Ey7bUBUijqiAQ5ful+aKANEePdshgapphfJstRAz+ppjZsBwN2XKFtVbY5znzDZ\n\tdukvqB8A9KXOt0a4TT/q9JPE2ivlhp2ldhAr8LUcBNSLNTKKR5HwOSrMVxTaP6Pq\n\tZwbeW6NuDsPCLk0YpkcT13YrDK29IObZMr6g7UkZ3UD0w99OveQdimMvslIwd/sp\n\twP9oRefqZ1vhyllOA0SFmSlzTk7iSLrBzoGAXALhGpUdxN1LsCIvDZGydm7YtOGX\n\tGOu+VTA6odHOB5urEEGn+dMPlF1qEcqirvYFmnamcnjrhn2nbxLn8jz2MRlNTGPt\n\t88sAEQEAAYkCRAQYAQoADwIbLgUCV9WeGQUJCcBLJgEpwF0gBBkBCgAGBQJR17nz\n\tAAoJEE1lxursh97NIVsH/i1edqJvxH2naj49hMp3m8OSUj0cOkTA1rebeF33YFLn\n\tXqHUdL1DelFQIZ8zXcS5E5iB7OceqxNjoJaGU0k4yXg4IU52xtHcwM2FqxtRNMds\n\t4yb+Hpopm2oLl9lsnA/Rm9pqNGoVN6Hc/mbueYpVxB1jKFqH1mX3+G/h5Z6YzPXg\n\tjf6F8SLgM1kfJA7zb77Gghe5+xtYNGqoqRFne3YqHApXYfTbOFxr+5+32v2m7ib/\n\tOI4p5Zq/y/F5+QLn+phGsWeYrmGCalyTzxCZvDgtgDsucqF55G8EIxiPQ/IQrs+y\n\t/VuL+nvIZjKJO8X8r0AXNk7HA/KTxTUkRYArAwLj+skJEAIeA8rdpTl3nnYH/1z6\n\ttNBVVDRl/jU9m0yj4PpNMZUjd+t0jH3WzMrqKbN7/Io8kFCLJdmg4+97tXjVtRbR\n\tCWw7K43cOpQchXh3t+cwtcfdJd6TOqJCO21laQL0CBIZlNS9lQ7c4J9eew3MKe4Y\n\tD22kOes/SXAIONU/KNP+aLXy8iMvXcxKe3vsZj4g4Huk4+mXkoFIrVit/tm7PIqm\n\tVZd8lFRK841qZdL+vc/JQR8b8o3CtTbBJ4KO/sylw0M0EHsIQv2TL//NABiCuob/\n\tgjpASGi/ZqGVKl/twu9ZaL3Z07uOGOlAlvU5VVlA/1gf39BoUA1ZL5loPNlERPIF\n\toqPZ3VIfhRjqQbvOnDU=\n\t=OeUJ\n\t-----END PGP PUBLIC KEY BLOCK-----\n\n## Checksum Verification\n\nIf you would like to verify the checksum of a fabio download,\nplease note that only the `sha256` file is signed by the GPG key\nbelow. The binaries themselves are not signed, but rather hashed.\nTo verify the integrity of a particular binary:\n\nDownload the binary, `sha256`, and `sha256.sig` files\nVerify the `sha256` file is properly signed\nVerify the `sha256` in the file matches the binary\n\nFor example:\n\n```\n# This is the public key from above - one-time step.\ngpg --import fabiolb.asc\n\n# Download the binary and signature files.\ncurl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15-linux_amd64\ncurl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15.sha256\ncurl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15.sha256.sig\n\n# Verify the signature file is untampered.\ngpg --verify fabio-1.5.14-go1.15.sha256.sig fabio-1.5.14-go1.15.sha256\n# Verify the SHASUM matches the binary.\ngrep linux_amd64 fabio-1.5.14-go1.15.sha256 | shasum -a 256 -c -\n```\n\n## Note\n\nParts of this text have been taken from the [HashiCorp Security Page](https://www.hashicorp.com/security.html)\nwhich describes the procedure which is also used for fabio.\n"
  },
  {
    "path": "docs/content/faq/why-fabio.md",
    "content": "---\ntitle: \"Why 'Fabio'?\"\n---\n\nWhen I was writing fabio my son was watching \"Finding Nemo\" almost every day and Dory keeps getting Nemos' name wrong. \n\nOne of the names she called him was \"Fabio\". Hence the name.\n\nSo no, this isn't about the first male super-model ...\n"
  },
  {
    "path": "docs/content/feature/_index.md",
    "content": "---\ntitle: \"Features\"\nweight: 200\n---\n\nThe following list provides a list of features supported by fabio. \n\n * [Access Logging](/feature/access-logging/) - customizable access logs\n * [Access Control](/feature/access-control/) - route specific access control\n * [Certificate Stores](/feature/certificate-stores/) - dynamic certificate stores like file system, HTTP server, [Consul](https://consul.io/) and [Vault](https://vaultproject.io/)\n * [Compression](/feature/http-compression/) - GZIP compression for HTTP responses\n * [Docker Support](/feature/docker/) - Official Docker image, Registrator and Docker Compose example\n * [Dynamic Reloading](/feature/dynamic-reloading/) - hot reloading of the routing table without downtime\n * [Graceful Shutdown](/feature/graceful-shutdown/) - wait until requests have completed before shutting down\n * [HTTP Header Support](/feature/http-headers/) - inject some HTTP headers into upstream requests\n * [HTTPS Upstreams](/feature/https-upstream/) - forward requests to HTTPS upstream servers\n * [Metrics Support](/feature/metrics/) - support for Graphite, StatsD/DataDog and Circonus\n * [PROXY Protocol Support](/feature/proxy-protocol/) - support for HA Proxy PROXY protocol for inbound requests (use for Amazon ELB)\n * [Path Stripping](/feature/http-path-stripping/) - strip prefix paths from incoming requests\n * [Path Prepending](/feature/http-path-prepending/) - prepend a prefix path on to incoming requests\n * [Server-Sent Events/SSE](/feature/sse/) - support for Server-Sent Events/SSE\n * [TCP Proxy Support](/feature/tcp-proxy/) - raw TCP proxy support\n * [TCP-SNI Proxy Support](/feature/tcp-sni-proxy/) - forward TLS connections based on hostname without re-encryption\n * [HTTPS TCP-SNI Proxy Support](/feature/https-tcp-sni-proxy/) - forward TLS connections based on hostname without re-encryption, or fallback to fabio terminating TLS and path routing as a fallback\n * [Traffic Shaping](/feature/traffic-shaping/) - forward N% of traffic upstream without knowing the number of instances\n * [Web UI](/feature/web-ui/) - web ui to examine the current routing table\n * [Websocket Support](/feature/websockets/) - websocket support\n * [BGP Support](/feature/bgp) - bgp support\n"
  },
  {
    "path": "docs/content/feature/access-control.md",
    "content": "---\ntitle: \"Access Control\"\nsince: \"1.5.8\"\n---\n\nfabio supports basic ip centric access control per route.  You may\nspecify one of `allow` or `deny` options per route to control access.\nCurrently only source ip control is available.\n\n<!--more-->\n\nTo allow access to a route from clients within the `192.168.1.0/24`\nand `fe80::/10` subnet you would add the following option:\n\n```\nallow=ip:192.168.1.0/24,ip:fe80::/10\n```\n\nWith this specified only clients sourced from those two subnets will\nbe allowed.  All other requests to that route will be denied.\n\n\nInversely, to deny a specific set of clients you can use the\nfollowing option syntax:\n\n```\ndeny=ip:fe80::1234,100.123.0.0/16\n```\n\nWith this configuration access will be denied to any clients with\nthe `fe80::1234` address or coming from the `100.123.0.0/16` network.\n\nSingle host addresses (addresses without a prefix) will have a\n`/32` prefix, for IPv4, or a `/128` prefix, for IPv6, added automatically.\nThat means `1.2.3.4` is equivalent to `1.2.3.4/32` and `fe80::1234`\nis equivalent to `fe80::1234/128` when specifying\naddress blocks for `allow` or `deny` rules.\n\nThe source ip used for validation against the defined ruleset is\ntaken from information available in the request.\n\nFor `HTTP` requests the client `RemoteAddr` is always validated\nfollowed by all elements of the `X-Forwarded-For` header, if\npresent.  When all of these elements match an `allow` the request\nwill be allowed; similarly when any element matches a `deny` the\nrequest will be denied.\n\nFor `TCP` requests the source address of the network socket\nis used as the sole paramater for validation.\n\nIf the inbound connection uses the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)\nto transmit the true source address of the client then it will\nbe used for both `HTTP` and `TCP` connections for validating access.\n\n"
  },
  {
    "path": "docs/content/feature/access-logging.md",
    "content": "---\ntitle: \"Access Logging\"\nsince: \"1.4.1\"\n---\n\nSupport for writing access logs for HTTP requests\nin the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format)\nor the [Combined Log Format](https://httpd.apache.org/docs/2.4/logs.html#combined)\nor a custom format to stdout.\n\n<!--more-->\n\nBy default, access logs are disabled. To enable them set `log.access.target=stdout`. This will\nwrite access logs in the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) to stdout. The\nstandard fabio logs are still written to stderr.\n\nThe log format can be controlled with the `log.access.format` parameter which\nis either `common`, `combined` - which outputs the [Combined Log Format](https://httpd.apache.org/docs/2.4/logs.html#combined) - or a custom\nformat string which is fully described in\n[fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties#L374-L421).\n\n```\n# log.access.format configures the format of the access log.\n#\n# If the value is either 'common' or 'combined' then the logs are written in\n# the Common Log Format or the Combined Log Format as defined below:\n#\n# 'common':   $remote_host - - [$time_common] \"$request\" $response_status $response_body_size\n# 'combined': $remote_host - - [$time_common] \"$request\" $response_status $response_body_size \"$header.Referer\" \"$header.User-Agent\"\n#\n# Otherwise, the value is interpreted as a custom log format which is defined\n# with the following parameters. Providing an empty format when logging is\n# enabled is an error. To disable access logging leave the log.access.target\n# value empty.\n#\n#   $header.<name>           - request http header (name: [a-zA-Z0-9-]+)\n#   $remote_addr             - host:port of remote client\n#   $remote_host             - host of remote client\n#   $remote_port             - port of remote client\n#   $request                 - request <method> <uri> <proto>\n#   $request_args            - request query parameters\n#   $request_host            - request host header (aka server name)\n#   $request_method          - request method\n#   $request_scheme          - request scheme\n#   $request_uri             - request URI\n#   $request_url             - request URL\n#   $request_proto           - request protocol\n#   $response_body_size      - response body size in bytes\n#   $response_status         - response status code\n#   $response_time_ms        - response time in S.sss format\n#   $response_time_us        - response time in S.ssssss format\n#   $response_time_ns        - response time in S.sssssssss format\n#   $time_rfc3339            - log timestamp in YYYY-MM-DDTHH:MM:SSZ format\n#   $time_rfc3339_ms         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format\n#   $time_rfc3339_us         - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format\n#   $time_rfc3339_ns         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format\n#   $time_unix_ms            - log timestamp in unix epoch ms\n#   $time_unix_us            - log timestamp in unix epoch us\n#   $time_unix_ns            - log timestamp in unix epoch ns\n#   $time_common             - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ\n#   $upstream_addr           - host:port of upstream server\n#   $upstream_host           - host of upstream server\n#   $upstream_port           - port of upstream server\n#   $upstream_request_scheme - upstream request scheme\n#   $upstream_request_uri    - upstream request URI\n#   $upstream_request_url    - upstream request URL\n#   $upstream_service        - name of the upstream service\n#\n# The default is\n#\n# log.access.format = common\n```\n"
  },
  {
    "path": "docs/content/feature/authorization.md",
    "content": "---\ntitle: \"Authorization\"\nsince: \"1.5.11\"\n---\n\nfabio supports basic http authorization on a per-route basis.\n\n<!--more-->\n\nAuthorization schemes are configured with the `proxy.auth` option.\nYou can configure one or multiple schemes.\n\nEach authorization scheme is configured with a list of\nkey/value options.\n\n    name=<name>;type=<type>;opt=arg;opt[=arg];...\n\nEach scheme must have a **unique name** which is then\nreferenced in a route configuration.\n\n    proxy.auth = name=myauth;type=...\n\nWhen you configure the route, you must reference the unique name for the authorization scheme:\n\n    route add svc / https://127.0.0.1:8080 auth=<name>\n\n    urlprefix-/ proto=https auth=<name>\n\nThe following types of authorization schemes are available:\n\n* [`basic`](#basic): legacy store for a single TLS and a set of client auth certificates\n\nAt the end you also find a list of [examples](#examples).\n\n### Basic\n\nThe basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits.\n\nThe `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero.\nNote: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored.\n\n    name=<name>;type=basic;file=<file>;realm=<realm>;refresh=<interval>\n\nSupported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd)\n\n#### Examples\n\n    # single basic auth scheme\n    name=mybasicauth;type=basic;file=p/creds.htpasswd;\n\n    # single basic auth scheme with refresh interval set to 30 seconds\n    name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s\n\n    # basic auth with multiple schemes\n    proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s,\n                 name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm\n"
  },
  {
    "path": "docs/content/feature/bgp.md",
    "content": "---\ntitle: \"BGP\"\nsince: \"1.6.3\"\n---\n\nNOTE: This feature does not work on Windows at present since the gobgp project\ndoes not support windows.\n\n\nThis feature integrates the functionality of [gobgpd](https://github.com/osrg/gobgp)\nwith fabio.  This is particularly useful in the scenario where we are using\nanycast IP addresses and want to dynamically advertise to upstream routers\nwhen we're ready to receive traffic.  In the past, we've used external router\npackages such as quagga or frr to handle this for us, but it's potentially\nmessy to make sure that the route advertisement stops if fabio goes down,\nand the bgp daemon is started back up once fabio is running again.\nBy integrating the bgp advertisement with the proxy server, we've made\nsure that when fabio goes down, the route is no longer advertised and \ntraffic can be sent to other fabio instances accordingly.  When fabio is back up,\nthe route is advertised again.  \n\nFurther, the gobgp [command line client](https://github.com/osrg/gobgp/blob/master/docs/sources/cli-command-syntax.md) \nis fully supported by enabling \nthe [bgp.enablegrpc](/ref/bgp.enablegrpc/) option.\n\n[Multihop](/ref/bgp.peers/) is supported, where fabio may not be\non the same subnet as neighbor.\n\nTo enable BGP, you must at a minimum:\n* Set up an anycast interface on the host with a /32 address.  On linux, the dummy interface type is a good option \n  since it's supported using network manager.  Another option is hanging this address off of loopback.\n* Configure the neighbor / peer / upstream router to allow us to peer, and to allow our anycast as a prefix it will \n  accept\n* Set [bgp.enabled](/ref/bgp.enabled/)=true\n* Configure the [bgp.asn](/ref/bgp.asn/) to be our router's Asn - probably use a private ASN here\n* Configure the [bgp.routerid](/ref/bgp.routerid/) to be our router's IP address (i.e., not the anycast address, \n  something unique).  This will be the default nexthop of all routes we publish.\n* Configure the [bgp.peers](/ref/bgp.peers/) for at least one nieghbor.\n* Configure the [bgp.anycastaddresses](/ref/bgp.anycastaddresses) for at least one anycast address.\n\nThis will embed a gobgpd instance inside of fabio on startup and it will publish the configure anycast addresses.  \nIt will also configure a [gobgpd policy](https://github.com/osrg/gobgp/blob/master/docs/sources/policy.md)\nthat will reject all incoming prefixes from neighbors.\n\nAlternatively, for more advanced use cases, you can reference an [external gobgpd config file](/ref/bgp.gobgpdcfgfile/) \nthat will override many of the options set in the fabio config, including the policy\nblocking us from accepting prefixes from neighbors.  You still need to specify the bgp.grpc\noptions from the fabio config since there is no analog in the gobgpd config file. \nYou may still specify bgp anycastaddresses or bgp.peers from \nthe fabio config, but we ignore anything\nthat would be specified in the global section of the gobgpd config file, including router ID and\nthe ASN.  Even If the bgp.gobgpdcfgfile value is set, fabio will still honor any values \nconfigured for bgp.anycastaddresses or bgp.peers.  These will be processed after the config\nfile is processed.\n\n\n### Note\nFor situations where multiple fabio instances are running with the same anycast address\nin the same datacenter, or in any other situation where the path distance\nis the same and load balancing across multiple fabio instances is desired, \nthe details of ECMP configuration is outside the scope of\nthis document as configuration would vary greatly depending on the \ndetails of the upstream router.\n\n"
  },
  {
    "path": "docs/content/feature/certificate-stores.md",
    "content": "---\ntitle: \"Certificate Stores\"\nsince: \"1.2\"\n---\n\nSupport for dynamic certificate stores which allow you to store certificates in\na central place and update them at runtime or generate them on the fly without\nrestart. You can store certificates in files, directories, on HTTP\nservers in [Consul](https://consul.io/) or in\n[Vault](https://vaultproject.io/).\nYou can use [Vault](https://vaultproject.io/) to generate certificates on the fly.\n\n<!--more-->\n\nStarting with version 1.2 fabio has support for dynamic certificate stores\nwhich allow you to store certificates in a central place and update them at\nruntime without restarting fabio. As of Go <= 1.7 only TLS certificates can be\nchanged at runtime. For updating client auth certificates [golang issue\n16066](https://github.com/golang/go/issues/16066) is open.\n\nCertificate stores are configured with the `proxy.cs` option.\nYou can configure one or multiple stores.\n\nEach certificate source is configured with a list of\nkey/value options.\n\n    cs=<name>;type=<type>;opt=arg;opt[=arg];...\n\nEach source must have a **unique name** which is then\nreferenced in a listener configuration.\n\n    proxy.cs = cs=mycerts;type=...\n    proxy.addr = 1.2.3.4:9999;cs=mycerts;...\n\nAll certificates must be provided in **PEM format**\n\nThe following types of certificate sources are available:\n\n * [`file`](#file): legacy store for a single TLS and a set of client auth certificates\n * [`path`](#path): load certificates from a directory (e.g. managed by puppet/chef/ansible/...)\n * [`http`](#http): load certificates from an HTTP server\n * [`consul`](#consul) : load certificates from [Consul](https://consul.io/) KV store\n * [`vault`](#vault) : load certificates from [Vault](https://vaultproject.io/)\n\nAll certificate stores offer a set of [common options](#common-options). If you want to use\nclient certificate authentication with an Amazon API gateway check the `caupgcn` option there.\n\nAt the end you also find a list of [examples](#examples).\n\n### File\n\nThe file certificate source supports one certificate which is loaded at\nstartup and is cached until the service exits.\n\nThe `cert` option contains the path to the certificate file. The `key`\noption contains the path to the private key file. If the certificate file\ncontains both the certificate and the private key the `key` option can be\nomitted. The `clientca` option contains the path to one or more client\nauthentication certificates.\n\n##### Example\n\n    cs=<name>;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem\n\n### Path\n\nThe path certificate source loads certificates from a directory in\nalphabetical order and refreshes them periodically.\n\nThe `cert` option provides the path to the TLS certificates and the\n`clientca` option provides the path to the certificates for client\nauthentication.\n\nTLS certificates are stored either in one or two files:\n\n    www.example.com.pem or www.example.com-{cert,key}.pem\n\nTLS certificates are loaded in alphabetical order and the first certificate\nis the default for clients which do not support SNI.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nThe default refresh interval is 3 seconds and cannot be lower than 1 second\nto prevent busy loops. To load the certificates only once and disable\nautomatic refreshing set `refresh` to zero.\n\n##### Example\n\n    cs=<name>;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s\n\n### HTTP\n\nThe http certificate source loads certificates from an HTTP/HTTPS server.\n\nThe `cert` option provides a URL to a text file which contains all files\nthat should be loaded from this directory. The filenames follow the same\nrules as for the path source. The text file can be generated with:\n\n    ls -1 *.pem > list\n\nThe `clientca` option provides a URL for the client authentication\ncertificates analogous to the `cert` option.\n\nAuthentication credentials can be provided in the URL as request parameter,\nas basic authentication parameters or through a header.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nThe default refresh interval is 3 seconds and cannot be lower than 1 second\nto prevent busy loops. To load the certificates only once and disable\nautomatic refreshing set `refresh` to zero.\n\n##### Example\n\n    cs=<name>;type=http;cert=https://host.com/path/to/cert/list&token=123\n    cs=<name>;type=http;cert=https://user:pass@host.com/path/to/cert/list\n    cs=<name>;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234\n\n### Consul\n\nThe consul certificate source loads certificates from [Consul](https://consul.io/).\n\nThe `cert` option provides a KV store URL where the the TLS certificates are\nstored.\n\nThe `clientca` option provides a URL to a path in the KV store where the the\nclient authentication certificates are stored.\n\nThe filenames follow the same rules as for the [`path`](#path) source.\n\nThe TLS certificates are updated automatically whenever the KV store\nchanges. The client authentication certificates cannot be updated\nautomatically since Go does not provide a mechanism for that yet.\n(See [golang issue 16066](https://github.com/golang/go/issues/16066))\n\n##### Example\n\n    cs=<name>;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123\n\n### Vault\n\nThe Vault certificate store uses HashiCorp Vault as the certificate\nstore.\n\nThe `cert` option provides the path to the TLS certificates and the\n`clientca` option provides the path to the certificates for client\nauthentication.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nCertificate has to be stored in value. It means you have to write your cert into a *cert* and *key* fields of secret, that has to be your domain name.\nExample:\n```\nvault write secret/fabio/certs/www.domain.com cert=@cert.pem key=@key.pem\n```\n\nThe path to vault must be provided in the `VAULT_ADDR` environment\nvariable. The token must be provided in the `VAULT_TOKEN` environment\nvariable.\n\n**fabio versions <= 1.2.1** require a token with root and/or sudo privileges to create an orphan\ntoken for itself. This required fabio to have more privileges than it needs\nand it also prevented revoking the fabio token if the parent token was revoked.\nTherefore, supplying a token with root and/or sudo privileges is now deprecated\nand will be removed in a later release.\n\n**fabio versions > 1.2.1** will no longer attempt to create a token itself and\ninstead solely rely on the provided token. The provided token can be an orphan\nand should be renewable for the duration fabio is expected to run. It is\nrecommended not to set the `explicit_max_ttl` unless fabio is restarted\nbefore that time expires.\n\nfabio needs the following policies set on the path where the\ncertificates are stored, for example:\n\n      # For Vault < 0.7\n      path \"secret/fabio/cert\" {\n        capabilities = [\"list\"]\n      }\n\n      # For Vault >= 0.7; note the trailing slash\n      path \"secret/fabio/cert/\" {\n        capabilities = [\"list\"]\n      }\n\n      path \"secret/fabio/cert/*\" {\n        capabilities = [\"read\"]\n      }\n\n##### Example\n\n    cs=<name>;type=vault;cert=secret/fabio/certs\n\n### Common options\n\nAll certificate stores support the following options:\n\n * `caupgcn` : Upgrade a self-signed client auth certificate with this common-name\n            to a CA certificate. Typically used for self-singed certificates\n            for the Amazon AWS API Gateway certificates which do not have the\n            CA flag set which makes them unsuitable for client certificate\n            authentication in Go. For the AWS API Gateway set this value\n            to 'ApiGateway' to allow client certificate authentication.\n            This replaces the deprecated parameter 'aws.apigw.cert.cn'\n            which was introduced in version 1.1.5.\n\n### Examples\n\n     # file based certificate source\n     proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem\n\n     # path based certificate source\n     proxy.cs = cs=some-name;type=path;cert=path/to/certs\n\n     # HTTP certificate source\n     proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs\n\n     # Consul certificate source\n     proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123\n\n     # Vault certificate source\n     proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs\n\n     # Multiple certificate sources\n     proxy.cs = cs=srcA;type=path;cert=path/to/certs,\\\n                cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs\n\n     # path based certificate source for AWS Api Gateway\n     proxy.cs = cs=some-name;type=path;cert=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway\n"
  },
  {
    "path": "docs/content/feature/docker.md",
    "content": "---\ntitle: \"Docker Support\"\nsince: \"1.0\"\n---\n\nTo run fabio within Docker use the official Docker image `fabiolb/fabio` and\nmount your own config file to `/etc/fabio/fabio.properties`\n\n    docker run -d -p 9999:9999 -p 9998:9998 -v $PWD/fabio/fabio.properties:/etc/fabio/fabio.properties fabiolb/fabio\n\nIf you want to run the Docker image with one or more SSL certificates then\nyou can store your configuration and certificates in `/etc/fabio` and mount\nthe entire directory, e.g.\n\n    $ cat ~/fabio/fabio.properties\n    proxy.addr=:443;/etc/fabio/ssl/mycert.pem;/etc/fabio/ssl/mykey.pem\n\n    docker run -d -p 443:443 -p 9998:9998 -v $PWD/fabio:/etc/fabio fabiolb/fabio\n\nThe official Docker image contains the root CA certificates from a recent and updated\nUbuntu 12.04.5 LTS installation.\n\n### Registrator\n\nIf you use Gliderlabs [Registrator](https://github.com/gliderlabs/registrator) to register your services\nyou can pass the `urlprefix-` tags via the `SERVICE_TAGS` environment variable as follows:\n\n```\n$ docker run -d \\\n    --name=registrator \\\n    --net=host \\        \n    --volume=/var/run/docker.sock:/tmp/docker.sock \\\n    gliderlabs/registrator:latest \\\n    consul://localhost:8500\n\n$ docker run -d -p 80:8000 \\\n    -e SERVICE_8000_CHECK_HTTP=/foo/healthcheck  \\\n    -e SERVICE_8000_NAME=foo \\\n    -e SERVICE_CHECK_INTERVAL=10s \\\n    -e SERVICE_CHECK_TIMEOUT=5s  \\\n    -e SERVICE_TAGS=urlprefix-/foo \\\n    test/foo\n```\n\n### Docker Compose\n\nIf you are using [Docker compose](https://docs.docker.com/compose/) you can add the `SERVICE_TAGS`\nto the `environment` section as follows:\n\n    bar:\n      environment:\n        - SERVICE_TAGS=urlprefix-/bar\n\n"
  },
  {
    "path": "docs/content/feature/dynamic-reloading.md",
    "content": "---\ntitle: \"Dynamic Reloading\"\nsince: \"1.0\"\n---\n\nfabio builds the routing table from the Consul service registrations, health\ncheck status and the user provided `route` commands stored in the Consul KV\nstore. This is **the** core feature of fabio - the reason it exists.\n\nThe cluster wide state is stored in the Consul Raft log which provides a\nconsistent view of the available and healthy services in the cluster. \n\nWhen the Raft log changes fabio is notified and downloads the list of\nhealthy services and the user defined routes from the KV store and re-builds\nthe routing table.\n\nOnce the new routing table has been built it is atomically swapped with the\nactive routing table without any service interruption. Existing connections\nremain open and running requests are served even if the new routing table no\nlonger contains that route. \n\nRegistering or de-registering a service, setting a node to maintenance mode,\nfailing or passing of a health check for a service, or writing data into the\nConsul KV store all trigger an automatic reload of the fabio routing table for\nall fabio nodes in the cluster.\n\nThis all happens automatically, with no downtime, or manual intervention.\n"
  },
  {
    "path": "docs/content/feature/graceful-shutdown.md",
    "content": "---\ntitle: \"Graceful Shutdown\"\nsince: \"1.0\"\n---\n\nfabio supports a graceful shutdown timeout during which new requests will\nreceive a `503 Service Unavailable` response while the active requests can\ncomplete. See the `proxy.shutdownwait` option in the\n[fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties)\nfile.\n"
  },
  {
    "path": "docs/content/feature/grpc-proxy.md",
    "content": "---\ntitle: \"GRPC Proxy\"\nsince: \"1.5.11\"\n---\n\nfabio can run a transparent GRPC proxy which dynamically forwards an incoming\nRPC on a given port to services which advertise rpc service or method. To use GRPC\nproxy support the service needs to advertise `urlprefix-/my.service/Method proto=grpc` in\nConsul. In addition, fabio needs to be configured with a grpc listener:\n\n```\nfabio -proxy.addr ':1234;proto=grpc'\n```\n\nAs per the HTTP/2 spec, the host header is not required, so host matching is not supported for GRPC proxying.\n\nTo route to a particular grpc service(s), set the 'dsthost'='name' in metadata, the grpc traffic will be routed to the service advertising `urlprefix-name/my.service/Method proto=grpc`.\n\nFor example in Go grpc request:\n```\n\tmd := metadata.Pairs(\"dsthost\", \"testonly\")\n\tctx := metadata.NewOutgoingContext(context.Background(), md)\n```\nTo make RPC using the context with the metadata will try to access the service with tag\n```\n urlprefix-testonly/my.service/Method proto=grpc\n```\n\nfirst, if no such service exits, fall back to the service with tag \n```\n urlprefix-/my.service/Method proto=grpc\n```\n\nEquivalent metadata in Java:\n```\npublic class GrpcClientHeaderInterceptor implements ClientInterceptor {\n\n    private String dstHost;\n\n    public GrpcClientHeaderInterceptor(String dstHost) {\n        this.dstHost = dstHost;\n    }\n\n    @VisibleForTesting\n    static final Metadata.Key<String> CUSTOM_HEADER_KEY =\n            Metadata.Key.of(\"dsthost\", Metadata.ASCII_STRING_MARSHALLER);\n\n    @Override\n    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,\n                                                               CallOptions callOptions, Channel next) {\n        ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);\n        return new SimpleForwardingClientCall<ReqT, RespT>(call) {\n            @Override\n            public void start(Listener<RespT> responseListener, Metadata headers) {\n                /* put custom header */\n                headers.put(CUSTOM_HEADER_KEY, \"testonly\");\n            }\n\n        };\n    }\n}\n```\n\n\nGRPC proxy support can be combined with [Certificate Stores](/feature/certificate-stores/) to provide TLS termination on fabio. Configure `proxy.addr` with `proto=grpcs`.\n\n```\nfabio -proxy.cs 'cs=ssl;type=path;path=/etc/ssl' -proxy.addr ':1234;proto=grpcs;cs=ssl'\n```\n\nTo support TLS upstream servers add the `proto=grpcs` option to the\n`urlprefix-` tag. The current implementation uses the clientca specified in the [Certificate Store](/feature/certificate-stores/) for the listener. To disable certificate\nvalidation for a target set the `tlsskipverify=true` option.\n\n```\nurlprefix-/foo proto=grpcs\nurlprefix-/foo proto=grpcs tlsskipverify=true\n```\n\nFor TLS upstream servers (when using the consul registry) fabio will direct your traffic to an advertised service IP. If your service certificate does not contain an IP SAN, the certificate verification will fail. You can set the override the server name in the tls config by setting `grpcservername=<servername>` in the `urlprefix-` tag.\n\n```\nurlprefix-/ proto=grpcs grpcservername=my.service.hostname\n```\n"
  },
  {
    "path": "docs/content/feature/http-compression.md",
    "content": "---\ntitle: \"HTTP Compression\"\nsince: \"1.3.4\"\n---\n\nEnable dynamic compression of responses when the client sets the\n`Accept-Encoding: gzip` header and the name of the requested file matches\na regular expression.\n\nTo configure which files should be compressed on the fly set configure\na regular expression in the `proxy.gzip.contenttype` property\n\n```\n# proxy.gzip.contenttype configures which responses should be compressed.\n#\n# By default, responses sent to the client are not compressed even if the\n# client accepts compressed responses by setting the 'Accept-Encoding: gzip'\n# header. By setting this value responses are compressed if the Content-Type\n# header of the response matches and the response is not already compressed.\n# The list of compressable content types is defined as a regular expression.\n# The regular expression must follow the rules outlined in golang.org/pkg/regexp.\n#\n# A typical example is\n#\n# proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$\n#\n# The default is\n#\n# proxy.gzip.contenttype =\n```\n"
  },
  {
    "path": "docs/content/feature/http-headers.md",
    "content": "---\ntitle: \"HTTP Header Support\"\nsince: \"1.1.3\"\n---\n\nIn addition, to injecting the `Forwarded` and `X-Real-Ip` headers the\n`X-Forwarded-For`, `X-Forwarded-Port` and `X-Forwarded-Proto` headers are added\nto HTTP(S) and Websocket requests. Custom headers for the ip address and\nprotocol can be configured with the `proxy.header.clientip`, `proxy.header.tls`\nand `proxy.header.tls.value` options.\n\nSince version 1.5.3 fabio also sets the `X-Forwarded-Host` header.\n"
  },
  {
    "path": "docs/content/feature/http-path-prepending.md",
    "content": "---\ntitle: \"HTTP Path Prepending\"\nsince: \"1.5.14\"\n---\n\nfabio supports prepending a path to the incoming request. If you want to\nforward `http://host/bar` as `http://host/foo/bar` you can add a `prepend=/foo`\noption to the route options as `urlprefix-/bar prepend=/foo`.\n\nPath prepending is done after path stripping. If you want to\nforward `http://host/foo/bar` as `http://host/baz/bar` you can add\n`prepend=/baz` and `strip=/foo` options to the route options as\n`urlprefix-/bar prepend=/baz strip=/foo`.\n"
  },
  {
    "path": "docs/content/feature/http-path-stripping.md",
    "content": "---\ntitle: \"HTTP Path Stripping\"\nsince: \"1.3.7\"\n---\n\nfabio supports stripping a path from the incoming request. If you want to\nforward `http://host/foo/bar` as `http://host/bar` you can add a `strip=/foo`\noption to the route options as `urlprefix-/foo/bar strip=/foo`.\n"
  },
  {
    "path": "docs/content/feature/http-redirects.md",
    "content": "---\ntitle: \"HTTP Redirects\"\nsince: \"1.5.4\"\n---\n\nTo redirect an HTTP request to another URL you can use the `redirect=<code>` option. The `code` is the\nHTTP status code used for the redirect response and must be between 300-399 for the route to be valid.\n\n\t# redirect /path to https://www.google.com/\n\troute add svc /path https://www.google.com/ opts \"redirect=301\"\n\nTo use the redirect with the `urlprefix-` tags you need to specify the target URL in after the code since\nthe target of the request is usually the address of the service that registers the tag.\n\n\turlprefix-/path redirect=301,https://www.google.com/\n\nIf you want to include the original request URI in the redirect target append the `$path` pseudo-variable\nto the target URL.\n\n\turlprefix-/path redirect=303,https://www.foo.com$path\n\nTo redirect from HTTP to HTTPS you must include the `host:port` of the HTTP endpoint:\n\n\troute add svc example.com:80/ https://example.com/ opts \"redirect=301\"\n"
  },
  {
    "path": "docs/content/feature/https-tcp-sni-proxy.md",
    "content": "---\ntitle: \"HTTPS TCP-SNI Proxy\"\nsince: \"1.5.14\"\n---\n\nfabio can run a TCP+SNI routing proxy on a listener, and have fallback to https functionality.\n This is effectively an amalgam of the TCP-SNI Proxy and the HTTPS functionality.\n \n To enable this feature configure a listener as follows:\n \n ```\n fabio -proxy.addr=':443;proto=https+tcp+sni;cs=somecertstore'\n ```\n \nFor host matches that are proto=tcp or have a scheme of tcp://, this will proxy TCP using SNI.\n\nYou would register your service in [Consul](https://consul.io) with a `urlprefix-` tag that\nmatches the host from the SNI extension for any services that should be proxied TCP (TLS\nterminated by upstream).  If the upstream service you'd like to proxy TCP responds to\n`https://foo.com/...` then you should register a `urlprefix-foo.com/ proto=tcp` tag for this\nservice.\n\nFor path based matching, you would do the typical `urlprefix-/path/` and this would cause\nfabio to terminate TLS using the cs= line specified in the config.\n"
  },
  {
    "path": "docs/content/feature/https-upstream.md",
    "content": "---\ntitle: \"HTTPS Upstream\"\nsince: \"1.4.2\"\n---\n\nTo support HTTPS upstream servers add the `proto=https` option to the\n`urlprefix-` tag. The current implementation requires that upstream\ncertificates need to be in the system root CA list. To disable certificate\nvalidation for a target set the `tlsskipverify=true` option.\n\n```\nurlprefix-/foo proto=https\nurlprefix-/foo proto=https tlsskipverify=true\n```\n\n"
  },
  {
    "path": "docs/content/feature/metrics.md",
    "content": "---\ntitle: \"Metrics\"\nsince: \"1.0.0 (Graphite), 1.2.1 (StatsD, DataDog, Circonus), 1.6.0 (Prometheus)\"\n---\n\nFabio collects metrics per route and service instance as well as running totals\nto avoid computing large amounts of metrics. The metrics can be sent to\n[Circonus](http://www.circonus.com), [Graphite](https://graphiteapp.org),\n[StatsD](https://github.com/etsy/statsd), [DataDog](https://www.datadoghq.com)\n(via statsd - or since v1.6.0 to native protocol with tag support) or stdout. See the `metrics.*`\noptions in the [fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties)\nfile.  Prometheus is also possible, but it works the reverse of the other metrics platforms. \nInstead of pushing data to a metrics server, prometheus expects to poll an endpoint for changes.\n\n### Configuring Prometheus Metrics\n\nTo configure prometheus metrics, you need to do the following:\n\n1) You must specify that prometheus is the [metrics.target](/ref/metrics.target/)\n2) You must configure a listener in [proxy.addr](/ref/proxy.addr/) with `proto=prometheus`\n3) (optional) override the \n[metrics.prometheus.path](/ref/metrics.prometheus.path/),\n[metrics.prometheus.subsystem](/ref/metrics.prometheus.subsystem/),\nand [metrics.prometheus.buckets](/ref/metrics.prometheus.buckets/). \n\n### Metrics info (for non-tagged backends, such as circonus and statsd_raw)\n\nFabio reports the following metrics:\n\nName                        | Type     | Description\n--------------------------- | -------- | -------------\n`{route}.rx`                | timer    | Number of bytes received by fabio for TCP target\n`{route}.tx`                | timer    | Number of bytes transmitted by fabio for TCP target\n`{route}`                   | timer    | Average response time for a route\n`http.status.code.{code}`   | timer    | Average response time for all HTTP(S) requests per status code\n`notfound`                  | counter  | Number of failed HTTP route lookups\n`requests`                  | timer    | Average response time for all HTTP(S) requests\n`grpc.requests`             | timer    | Average response time for all GRPC(S) requests\n`grpc.noroute`              | counter  | Number of failed GRPC route lookups\n`grpc.conn`                 | counter  | Number of established GRPC proxy connections\n`grpc.status.{code}`        | timer    | Average response time for all GRPC(S) requests per status code\n`tcp.conn`                  | counter  | Number of established TCP proxy connections\n`tcp.connfail`              | counter  | Number of TCP upstream connection failures\n`tcp.noroute`               | counter  | Number of failed TCP upstream route lookups\n`tcp_sni.conn`              | counter  | Number of established TCP+SNI proxy connections\n`tcp_sni.connfail`          | counter  | Number of failed TCP+SNI proxy connections\n`tcp_sni.noroute`           | counter  | Number of failed TCP+SNI upstream route lookups\n`ws.conn`                   | gauge    | Number of actively open websocket connections\n\n\n### Legend\n\n#### timer\n\nA timer counts events and provides an average throughput and latency number.\nDepending on the metrics provider the aggregation happens either in the metrics library\n(go-metrics: statsd, graphite) or in the system of the metrics provider (Circonus)\n\n#### counter\n\nA counter counts events and provides an monotonically increasing value.\n\n#### gauge\n\nA gauge provides a current value.\n\n#### {code}\n\n`{code}` is the three digit HTTP status code like `200`.\n\n#### {route}\n\n`{route}` is a shorthand for the metrics name generated for a route\nwith the `metrics.names` template defined in\n[fabio.properties](https://github.com/fabiolb/fabio/blob/master/fabio.properties)\n\n\n"
  },
  {
    "path": "docs/content/feature/proxy-protocol.md",
    "content": "---\ntitle: \"PROXY Protocol Support\"\nsince: \"1.1.3\"\n---\n\nfabio transparently supports the HA Proxy\n[PROXY protocol](http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) version 1\nwhich is used by HA Proxy,\n[Amazon ELB](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/enable-proxy-protocol.html)\nand others to transmit the remote address and port of the client without using headers.\n\nYou may control the behavior of PROXY protocol support with the following\noptions on the listener:\n\n* `pxyproto`: When set to 'true' the listener will respect upstream v1\n  PROXY protocol headers.\n  NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10.\n  This changed to off when this option was introduced with\n  the 1.5.11 release.\n  For more information about the PROXY protocol, please see:\n  http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt\n\n* `pxytimeout`: Sets PROXY protocol header read timeout as a duration (e.g. '250ms').\n  This defaults to 250ms if not set when 'pxyproto' is enabled.\n\nSee the comments in for `proxy.addr` in `fabio.properties` for more information.\n"
  },
  {
    "path": "docs/content/feature/sse.md",
    "content": "---\ntitle: \"Server Sent Events (SSE)\"\nsince: \"1.3\"\n---\n\nfabio detects [SSE](http://www.w3.org/TR/eventsource/) connections if the\n`Accept` header is set to `text/event-stream` and enables automatic flushing of\nthe response buffer to forward data to the client. The default is set to `1s`\nand can be configured with the `proxy.flushinterval` parameter.\n"
  },
  {
    "path": "docs/content/feature/tcp-dynamic-proxy.md",
    "content": "---\ntitle: \"TCP Dynamic Proxy\"\n---\n\nThe TCP dynamic proxy is similar to the TCP Proxy, but the listener is started from the Consul urlprefix tag.\nAlso, the service is defined with IP and port, so that multiple services can be defined on the load balancer using\nthe same TCP port.  Connections are forwarded to services based on the combination of ip:port\n\nTo use TCP Dynamic proxy support the service needs to advertise `urlprefix-127.0.0.1:1234 proto=tcp` in\nConsul. In addition, fabio needs to be configured with a placeholder for the proxy.addr.:\n\n```\nfabio -proxy.addr '0.0.0.0:0;proto=tcp-dynamic;refresh=5s'\n```\n\nThe TCP listener is started for the given TCP ports.  To use IP addressing to separate the services, matching IP\naddressed would need to be added to the loopback interface on the host."
  },
  {
    "path": "docs/content/feature/tcp-proxy.md",
    "content": "---\ntitle: \"TCP Proxy\"\nsince: \"1.4\"\n---\n\nfabio can run a transparent TCP proxy which dynamically forwards an incoming\nconnection on a given port to services which advertise that port. To use TCP\nproxy support the service needs to advertise `urlprefix-:1234 proto=tcp` in\nConsul. In addition, fabio needs to be configured to listen on that port:\n\n```\nfabio -proxy.addr ':1234;proto=tcp'\n```\n\nTCP proxy support can be combined with [Certificate Stores](/feature/certificate-stores/) to provide TLS termination on fabio.\n\n```\nfabio -proxy.cs 'cs=ssl;type=path;cert=/etc/ssl' -proxy.addr ':1234;proto=tcp;cs=ssl'\n```\n\n"
  },
  {
    "path": "docs/content/feature/tcp-sni-proxy.md",
    "content": "---\ntitle: \"TCP-SNI Proxy\"\nsince: \"1.3\"\n---\n\nfabio can run a transparent TCP proxy with SNI support which can forward any TLS connection\n**without re-encrypting the traffic**. fabio captures the `ClientHello` packet which is the\nfirst packet of the TLS handshake and extracts the server name from the SNI extension and\nuses it for finding the upstream server to forward the connection to. It then replays the\n`ClientHello` packet and then transparently forwards all traffic between client and server\nas a byte stream.\n\nTo enable this feature configure a listener as follows:\n\n```\nfabio -proxy.addr=':443;proto=tcp+sni'\n```\n\nto listen to more than 1 port separate with comma's (like if you want to do tcp and http listening):\n```\nfabio -proxy.addr ':9999,:19587;proto=tcp\n```\nThis will do normal fabio http(s) routing on port 9999 and TCP proxy on port 19587.\n\nand register your services in [Consul](https://consul.io/) with a `urlprefix-` tag that\nmatches the host from the SNI extension. If your server responds to `https://foo.com/...`\nthen you should register a `urlprefix-foo.com/` tag for this service. Note that the tag\nshould only contain  `<host>/` since path-based routing is not possible with this approach.\n"
  },
  {
    "path": "docs/content/feature/traffic-shaping.md",
    "content": "---\ntitle: \"Traffic Shaping\"\nsince: \"1.0\"\n---\n\nfabio allows to control the amount of traffic a set of service instances will\nreceive. You can use this feature to direct a fixed percentage of traffic to a\nnewer version of an existing service for testing (\"Canary testing\"). See\n[Config Language](../../cfg) for a complete description of the `route\nweight` command.\n\nThe following command will allocate 5% of traffic to `www.kjca.dev/auth/` to\nall instances of `service-b` which match tags `version-15` and `dc-fra`. This\nis independent of the number of actual instances running. The remaining 95%\nof the traffic will be distributed evenly across the remaining instances\npublishing the same prefix.\n\n```\nroute weight service-b www.kjca.dev/auth/ weight 0.05 tags \"version-15,dc-fra\"\n```\n\n### Vault Example\n\n[Vault](https://www.vaultproject.io) is a tool by [HashiCorp](https://www.hashicorp.com/) for managing secrets and protecting sensitive data. When running in HA mode, Vault will have a single active node which is responsible for responding the API requests. Fabio can be used to ensure traffic is routed to the correct server via traffic shaping.\n\nThe following command will allocate 100% of traffic to `vault.company.com` to the instance of `vault` which is registered with the tag `active`.\n\n```\nroute weight vault vault.company.com weight 1.00 tags \"active\"\n```\n"
  },
  {
    "path": "docs/content/feature/vault.md",
    "content": "---\ntitle: \"Vault Support\"\nsince: \"1.2 (Vault KV), 1.5.3 (Vault PKI)\"\n---\n\nfabio can use [Vault](https://vaultproject.io) as a secure key/value store to store certificates.\n\nAs of 1.5.3 fabio can use the PKI support of Vault to generate TLS certificates on demand.\nSee [fabio.properties](https://github.com/fabiolb/fabio/blob/master/fabio.properties) for details.\n\n"
  },
  {
    "path": "docs/content/feature/web-ui.md",
    "content": "---\ntitle: \"Web UI\"\nsincd: \"1.0\"\n---\n\nfabio supports a Web UI to examine the current routing table and manage the\nmanual overrides. By default it listens on `http://0.0.0.0:9998/` which can be\nchanged with the `ui.addr` option. The `ui.title` and `ui.color` options allow\ncustomization of the title and the color of the header bar.\n"
  },
  {
    "path": "docs/content/feature/websockets.md",
    "content": "---\ntitle: \"Websockets\"\nsince: \"1.0.5\"\n---\n\nfabio transparently supports Websocket connections by detecting the `Upgrade:\nwebsocket` header in the incoming HTTP(S) request.\n\nWebsocket support has been implemented with the websocket library from\n[golang.org/x/net/websocket](http://golang.org/x/net/websocket).\n\nYou can test the websocket support with the `demo/wsclient` and `demo/server` which\nimplements a simple echo server.\n\n    ./server -addr 127.0.0.1:5000 -name ws-a -prefix /echo -proto ws\n    ./wsclient -url ws://127.0.0.1:9999/echo\n\nYou can also run multiple web socket servers on different ports but the same endpoint.\n\nfabio detects on whether to forward the request as HTTP or WS based on the\nvalue of the `Upgrade` header. If the value is `websocket` it will attempt a\nwebsocket connection to the target. Otherwise, it will fall back to HTTP.\n\nOne limitation of the current implementation is that the accepted set of\nprotocols has to be symmetric across all services handling it. Only the\nfollowing combinations will work reliably:\n\n    svc-a and svc-b register /foo and accept only HTTP traffic there\n    svc-a and svc-b register /foo and accept only WS traffic there\n    svc-a and svc-b register /foo and accept both HTTP and WS traffic there\n\nThe following setup (or variations thereof) will not work reliably:\n\n    svc-a registers /foo and accept only WS traffic there\n    svc-b registers /foo and accept only HTTP traffic there\n\nThis is not a limitation of the routing itself but because the current\nconfiguration does not provide fabio with enough information to make the\nrouting decision since the services do not advertise the protocols they handle\non a given endpoint.\n\nThis does not look like a big restriction but is also not difficult to extend\nin a later version assuming there are use cases which require this behavior.\nFor now the services have to be symmetric in the protocols they accept.\n"
  },
  {
    "path": "docs/content/quickstart/_index.md",
    "content": "---\ntitle: \"Quickstart\"\nweight: 100\n---\n\n\n1. Install from source, binary, Docker or Homebrew.\n\n\t```\n\tgo get github.com/fabiolb/fabio                     (>= go1.15)\n\n\tbrew install fabio                                  (OSX/macOS stable)\n\n\tdocker pull fabiolb/fabio                           (Docker)\n\n\thttps://github.com/fabiolb/fabio/releases           (pre-built binaries)\n\t```\n\n2. Register your service in Consul.\n\n\tMake sure that each instance registers with a unique `ServiceID` and a service name **without spaces**.\n\n3. Register a health check in Consul as described [here](https://www.consul.io/docs/agent/checks.html).\n\n\tMake sure the health check is <button type=\"button\" class=\"btn btn-xs\n\tbtn-success\">PASSING</button> since fabio will only watch services which\n\thave a passing health check.\n\n4. Routes are stored in Consul [Service Tags](https://www.consul.io/docs/agent/services.html)\nand you need to add a separate `urlprefix-` tag for every `host/path` prefix the service serves.\n\t\n\tFor example, if your service handles `/user` and `/product` then add two tags `urlprefix-/user` and `urlprefix-/product`. \n\tYou can register as many prefixes as you want.\n\n\tfabio can forward HTTP, HTTPS and TCP traffic. Below are some configuration examples:\n\n\t```\n\t# HTTP/S examples\n\t# Make sure the prefix for HTTP routes contains at least one slash (/).\n\turlprefix-/css                                     # path route\n\turlprefix-i.com/static                             # host specific path route\n\turlprefix-mysite.com/                              # host specific catch all route\n\turlprefix-/foo/bar strip=/foo                      # path stripping (forward '/bar' to upstream)\n\turlprefix-/bar prepend=/foo                        # path prepending (forward '/foo/bar' to upstream)\n\turlprefix-/foo/bar proto=https                     # HTTPS upstream\n\turlprefix-/foo/bar proto=https tlsskipverify=true  # HTTPS upstream and self-signed cert\n\n\t# TCP examples\n\turlprefix-:3306 proto=tcp                          # route external port 3306\n\turlprefix-:3306 proto=tcp pxyproto=true            # enables PROXY protocol on outbount TCP connection\n\t\n\t# GRPC/S examples\n\turlprefix-/my.service/Method proto=grpc                      # method specific route\n\turlprefix-/my.service proto=grpc                             # service specific route\n\turlprefix-/my.service proto=grpcs                            # TLS upstream\n\turlprefix-/my.service proto=grpcs grpcservername=my.service  # TLS upstream with servername override\n\turlprefix-/my.service proto=grpcs tlsskipverify=true         # TLS upstream and self-signed cert\n\t```\n\n5. Start fabio without a config file\n\n\t```\n\t$ fabio\n\t```\n\n\tThis assumes that a Consul agent is running on `localhost:8500`.\n\n\tWatch the log output how fabio picks up the route to your service.\n\n\t**Note:** For running fabio in Docker [look here](/feature/docker/).\n\n6. Try starting/stopping your service to see how the routing table changes instantly.\n\n7. Test that you can access the upstream service via fabio\n\t\n\t```\n\t# for urlprefix-/foo\n\tcurl -i http://localhost:9999/foo\n\n\t# for urlprefix-mysite.com/foo\n\tcurl -i -H 'Host: mysite.com' http://localhost:9999/foo\n\n\t```\n\n8. Send all your HTTP traffic to fabio on port `9999`\n"
  },
  {
    "path": "docs/content/ref/_index.md",
    "content": "---\ntitle: \"Reference\"\nweight: 600\n---\n\nAll configuration options can be specified either \n\n* in the config file\n* as environment variable\n* as command line argument\n\nand are evaluated in that order. \n\n```\n# fabio.properties\nmetrics.target = stdout\n\n# correspondig env var (no prefix)\nmetrics_target=stdout ./fabio\n\n# env var with FABIO_ prefix (>= 1.2)\nFABIO_metrics_target=stdout ./fabio\n\n# env var with FABIO_ prefix (case-insensitive) (>= 1.2)\nFABIO_METRICS_TARGET=stdout ./fabio\n\n# command line argument (>= 1.2)\n./fabio -metrics.target stdout\n```\n"
  },
  {
    "path": "docs/content/ref/bgp.anycastaddresses.md",
    "content": "---\ntitle: \"bgp.anycastaddresses\"\n---\n\n`bgp.anycastaddresses` sets the anycast addresses we will advertise, \nseparated by comma.  Technically this will advertise any route prefix.  \nThese should already be configured on the host probably hung off loopback.\n For example, 192.168.5.3/32.\n\nThe default value is\n\n\tbgp.anycastaddresses =\n\nIf bgp is enabled, this must be defined.\n"
  },
  {
    "path": "docs/content/ref/bgp.asn.md",
    "content": "---\ntitle: \"bgp.asn\"\n---\n\n`bgp.asn` sets the asn ID of our router\n\nThe default value is\n\n\tbgp.asn = 65000\n"
  },
  {
    "path": "docs/content/ref/bgp.certfile.md",
    "content": "---\ntitle: \"bgp.certfile\"\n---\n\n`bgp.certfile` is the file path of the certificate, and is required if bgp.grpctls is set to true.\n\n\nThe default value is\n\n\tbgp.certfile =\n\n"
  },
  {
    "path": "docs/content/ref/bgp.enabled.md",
    "content": "---\ntitle: \"bgp.enabled\"\n---\n\n`bgp.enabled` enables the embedded gobgpd daemon\n\nThe default value is\n\n\tbgp.enabled = false\n"
  },
  {
    "path": "docs/content/ref/bgp.enablegrpc.md",
    "content": "---\ntitle: \"bgp.enablegrpc\"\n---\n\n`bgp.enablegrpc` enables the gobgp grpc interface.  \nTo be used with the gobgp command line client.\n\n\n\nThe default value is\n\n\tbgp.enablegrpc = false\n\n"
  },
  {
    "path": "docs/content/ref/bgp.gobgpdcfgfile.md",
    "content": "---\ntitle: \"bgp.gobgpdcfgfile\"\n---\n\n`bgp.gobgpdcfgfile` is the optional file path to a \ngobgpd [config file](https://github.com/osrg/gobgp/blob/master/docs/sources/configuration.md).\nThis overrides the global config\nitems, such as bgp.routerid, bgp.asn etc.  This also skips automatically adding gobgpd \n[policies](https://github.com/osrg/gobgp/blob/master/docs/sources/policy.md)\nthat restrict / disallow accepting prefixes from neighbors. Only use \nthis if you know what you're doing, this is to allow\nfor more flexibility than we expose directly with fabio.\n\nThe default value is\n\n\tbgp.gobgpdcfgfile =\n\n"
  },
  {
    "path": "docs/content/ref/bgp.grpclistenaddress.md",
    "content": "---\ntitle: \"bgp.grpclistenaddress\"\n---\n\n`bgp.grpclistenaddress` is the listen interface and port if bgp.enablegrpc is set to true.\n\n\n\nThe default value is\n\n\tbgp.grpclistenaddress = 127.0.0.1:50051\n\n"
  },
  {
    "path": "docs/content/ref/bgp.grpctls.md",
    "content": "---\ntitle: \"bgp.grpctls\"\n---\n\n`bgp.grpctls` is whether to enable TLS on the bgp grpc interface.\n\n\nThe default value is\n\n\tbgp.grpctls = false\n\n"
  },
  {
    "path": "docs/content/ref/bgp.keyfile.md",
    "content": "---\ntitle: \"bgp.keyfile\"\n---\n\n`bgp.keyfile` is the file path of the key file, and is required if bgp.grpctls is set to true.\n\n\nThe default value is\n\n\tbgp.keyfile =\n\n"
  },
  {
    "path": "docs/content/ref/bgp.listenaddresses.md",
    "content": "---\ntitle: \"bgp.listenaddresses\"\n---\n\n`bgp.listenaddresses` sets the listen addresses for bgp, separated by comma.\n\nThe default value is\n\n\tbgp.listenaddresses = 0.0.0.0\n\nwhich listens on all interfaces.\n"
  },
  {
    "path": "docs/content/ref/bgp.listenport.md",
    "content": "---\ntitle: \"bgp.listenport\"\n---\n\n`bgp.listenport` sets the listen ports for bgp communication from other routers.\n\nThe default value is\n\n\tbgp.listenport = 179\n\nThe default bgp port is 179.\n"
  },
  {
    "path": "docs/content/ref/bgp.nexthop.md",
    "content": "---\ntitle: \"bgp.nexthop\"\n---\n\n`bgp.nexthop` sets the next hop address.  \nIf not set, it uses the [bgp.routerid](/ref/bgp.routerid/) instead.\n\nThe default value is\n\n\tbgp.nexthop =\n\n"
  },
  {
    "path": "docs/content/ref/bgp.peers.md",
    "content": "---\ntitle: \"bgp.peers\"\n---\n\n`bgp.peers` sets the bgp peers we will advertise routes to.  This is required if bgp is enabled.\nbgp.peers is specified as a comma separated list of neighboraddress and asn pairs, i.e.\n\n    bgp.peers = address=1.2.3.4;asn=65001,address=5.6.7.8;asn=65002\n\nvalid parameters for peers are:\n \n    address              - required\n    port                 - optional, defaults to 179\n    asn                  - required\n    multihop             - optional, defaults to false\n    multihoplength       - optional, defaults to 2\n    password             - optional\n\nThe default value is\n\n\tbgp.peers =\n\n"
  },
  {
    "path": "docs/content/ref/bgp.routerid.md",
    "content": "---\ntitle: \"bgp.routerid\"\n---\n\n`bgp.routerid` is the router id (ip address) of this router.  \nThis is required if bgp is enabled.  This should be the unique IP\naddress, not any anycast.  This will also be used as\nthe default nexthop address unless [bgp.nexthop](/ref/bgp.nexthop/)\nis specified.\n\nThe default value is\n\n\tbgp.routerid =\n"
  },
  {
    "path": "docs/content/ref/glob.cache.size.md",
    "content": "---\ntitle: \"glob.cache.size\"\n---\n\n`glob.cache.size` Sets the globCache size used for matching on route lookups.\n\nThe default is\n\n\tglob.cache.size = 1000\n"
  },
  {
    "path": "docs/content/ref/glob.matching.disabled.md",
    "content": "---\ntitle: \"glob.matching.disabled\"\n---\n\n`glob.matching.disabled` disables glob matching on route lookups.\n\nValid options are `true`, `false`\n\nThe default is\n\n\tglob.matching.disabled = false\n"
  },
  {
    "path": "docs/content/ref/log.access.format.md",
    "content": "---\ntitle: \"log.access.format\"\n---\n\n`log.access.format` configures the format of the access log.\n\nIf the value is either `common` or `combined` then the logs are written in\nthe Common Log Format or the Combined Log Format as defined below:\n\n* `common`:   `$remote_host - - [$time_common] \"$request\" $response_status $response_body_size`\n* `combined`: `$remote_host - - [$time_common] \"$request\" $response_status $response_body_size \"$header.Referer\" \"$header.User-Agent\"`\n\nOtherwise, the value is interpreted as a custom log format which is defined\nwith the following parameters. Providing an empty format when logging is\nenabled is an error. \n\nTo disable access logging leave the `log.access.target` value empty.\n\n\t$header.<name>           - request http header (name: [a-zA-Z0-9-]+)\n\t$remote_addr             - host:port of remote client\n\t$remote_host             - host of remote client\n\t$remote_port             - port of remote client\n\t$request                 - request <method> <uri> <proto>\n\t$request_args            - request query parameters\n\t$request_host            - request host header (aka server name)\n\t$request_method          - request method\n\t$request_scheme          - request scheme\n\t$request_uri             - request URI\n\t$request_url             - request URL\n\t$request_proto           - request protocol\n\t$response_body_size      - response body size in bytes\n\t$response_status         - response status code\n\t$response_time_ms        - response time in S.sss format\n\t$response_time_us        - response time in S.ssssss format\n\t$response_time_ns        - response time in S.sssssssss format\n\t$time_rfc3339            - log timestamp in YYYY-MM-DDTHH:MM:SSZ format\n\t$time_rfc3339_ms         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format\n\t$time_rfc3339_us         - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format\n\t$time_rfc3339_ns         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format\n\t$time_unix_ms            - log timestamp in unix epoch ms\n\t$time_unix_us            - log timestamp in unix epoch us\n\t$time_unix_ns            - log timestamp in unix epoch ns\n\t$time_common             - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ\n\t$upstream_addr           - host:port of upstream server\n\t$upstream_host           - host of upstream server\n\t$upstream_port           - port of upstream server\n\t$upstream_request_scheme - upstream request scheme\n\t$upstream_request_uri    - upstream request URI\n\t$upstream_request_url    - upstream request URL\n\t$upstream_service        - name of the upstream service\n\nThe default is\n\n\tlog.access.format = common\n\n"
  },
  {
    "path": "docs/content/ref/log.access.target.md",
    "content": "---\ntitle: \"log.access.target\"\n---\n\n`log.access.target` configures where the access log is written to.\n\nOptions are `stdout`. If the value is empty no access log is written.\n\nThe default is\n\n\tlog.access.target =\n"
  },
  {
    "path": "docs/content/ref/log.level.md",
    "content": "---\ntitle: \"log.level\"\n---\n\n`log.level` configures the log level.\n\nValid levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`.\n\nThe default is\n\n\tlog.level = INFO\n"
  },
  {
    "path": "docs/content/ref/log.routes.format.md",
    "content": "---\ntitle: \"log.routes.format\"\n---\n\n`log.routes.format` configures the log output format of routing table updates.\n\nChanges to the routing table are written to the standard log. This option\nconfigures the output format:\n\n* `detail`:   detailed routing table as ascii tree\n* `delta`:    additions and deletions in config language\n* `all`:      complete routing table in config language\n\nThe default is\n\n\tlog.routes.format = delta\n\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.apiapp.md",
    "content": "---\ntitle: \"metrics.circonus.apiapp\"\n---\n\n`metrics.circonus.apiapp` configures the API token app to use when\nsubmitting metrics to Circonus. See: https://login.circonus.com/user/tokens\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`.\n\nThe default is\n\n\tmetrics.circonus.apiapp = fabio\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.apikey.md",
    "content": "---\ntitle: \"metrics.circonus.apikey\"\n---\n\n`metrics.circonus.apikey` configures the API token key to use when\nsubmitting metrics to Circonus. See: https://login.circonus.com/user/tokens\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`\nbut [metrics.circonus.submissionurl](/ref/metrics.circonus.submissionurl/) is specified}\n\nThe default is\n\n\tmetrics.circonus.apikey =\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.apiurl.md",
    "content": "---\ntitle: \"metrics.circonus.apiurl\"\n---\n\n`metrics.circonus.apiurl` configures the API URL to use when\nsubmitting metrics to Circonus. https://api.circonus.com/v2/\nwill be used if no specific URL is provided.\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`.\n\nThe default is\n\n\tmetrics.circonus.apiurl =\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.brokerid.md",
    "content": "---\ntitle: \"metrics.circonus.brokerid\"\n---\n\n`metrics.circonus.brokerid` configures a specific broker to use when\ncreating a check for submitting metrics to Circonus.\n\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`.\n\nOptional for public brokers, required for Inside brokers.\nOnly applicable if a check is being created.\n\nThe default is\n\n\tmetrics.circonus.brokerid =\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.checkid.md",
    "content": "---\ntitle: \"metrics.circonus.checkid\"\n---\n\n`metrics.circonus.checkid` configures a specific check to use when\nsubmitting metrics to Circonus.\n\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`.\n\nAn attempt will be made to search for a previously created check,\nif no applicable check is found, one will be created.\n\nThe default is\n\n\tmetrics.circonus.checkid =\n"
  },
  {
    "path": "docs/content/ref/metrics.circonus.submissionurl.md",
    "content": "---\ntitle: \"metrics.circonus.submissionurl\"\n---\n\n`metrics.circonus.submissionurl` configures a specific check submission url\nfor a Check API object of a previously created HTTPTRAP check.\n\nThis is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`\nbut [metrics.circonus.apikey](/ref/metrics.circonus.apikey/) is specified}.\n\n#### Example\n\n`http://127.0.0.1:2609/write/fabio`\n\nThe default is\n\n\tmetrics.circonus.submissionurl =\n"
  },
  {
    "path": "docs/content/ref/metrics.dogstatsd.addr.md",
    "content": "---\ntitle: \"metrics.dogstatsd.addr\"\n---\n\n`metrics.dogstatsd.addr` configures the host:port of the dogstatsd\nserver. \n\nThis is required when [metrics.target](/ref/metrics.target/) is set to `dogstatsd`.\n\nThe default is\n\n\tmetrics.dogstatsd.addr =\n"
  },
  {
    "path": "docs/content/ref/metrics.graphite.addr.md",
    "content": "---\ntitle: \"metrics.graphite.addr\"\n---\n\n`metrics.graphite.addr` configures the `host:port` of the Graphite server.\n\nThis is required when [metrics.target](/ref/metrics.target/) is set to `graphite`.\n\nThe default is\n\n\tmetrics.graphite.addr =\n"
  },
  {
    "path": "docs/content/ref/metrics.interval.md",
    "content": "---\ntitle: \"metrics.interval\"\n---\n\n`metrics.interval` configures the interval in which metrics are reported.\n\nThe default is\n\n\tmetrics.interval = 30s\n"
  },
  {
    "path": "docs/content/ref/metrics.names.md",
    "content": "---\ntitle: \"metrics.names\"\n---\n\n`metrics.names` configures the template for the route metric names\non backends that don't support tags.  This is used in circonus,\ngraphite and statsd_raw.  dogstatsd and prometheus ignore this.\nThe value is expanded by the [text/template](https://golang.org/pkg/text/template) package and provides\nthe following variables:\n\n* `Service`:   the service name\n* `Host`:      the host part of the URL prefix\n* `Path`:      the path part of the URL prefix\n* `TargetURL`: the URL of the target\n\nThe following additional functions are defined:\n\n* `clean`:     lowercase value and replace `.` and `:` with `_`\n\nGiven a route rule of\n\n\troute add testservice www.example.com/ http://10.1.2.3:12345/\n\nthe template variables are:\n\n\t.Service = testservice\n\t.Host = www.example.com\n\t.Path  = /\n\t.TargetURL.Host = 10.1.2.3:12345\n\nwhich results to the following metric name when using the default template:\n\n\ttestservice.www_example_com./.10_1_2_3_12345\n\nThe default is\n\n\tmetrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}\n"
  },
  {
    "path": "docs/content/ref/metrics.prefix.md",
    "content": "---\ntitle: \"metrics.prefix\"\n---\n\n`metrics.prefix` configures the template for the prefix of all reported metrics.\n\nEach metric has a unique name which is hard-coded to\n\n\tprefix.service.host.path.target-addr\n\nThe value is expanded by the text/template package and provides\nthe following variables:\n\n* `Hostname`:  the Hostname of the server\n* `Exec`:      the executable name of application\n\nThe following additional functions are defined:\n\n* `clean`:     lowercase value and replace `.` and `:` with `_`\n\nTemplate may include regular string parts to customize final prefix\n\n#### Example\n\nServer hostname: `test-001.something.com`\nBinary executable name: `fabio`\n\nThe template variables are:\n\n\t.Hostname =  test-001.something.com\n\t.Exec = fabio\n\nwhich results to the following prefix string when using the default template:\n\n\ttest-001_something_com.fabio\n\nThe default is\n\n\tmetrics.prefix = {{clean .Hostname}}.{{clean .Exec}}\n"
  },
  {
    "path": "docs/content/ref/metrics.prometheus.buckets.md",
    "content": "---\ntitle: \"metrics.prometheus.buckets\"\n---\n\n`metrics.prometheus.buckets` configures the time buckets for use with histograms, measured in seconds.\nfor instance, .005 is equivalent to 5ms.  There is an implied \"infinity\" bucket tacked on at the end.\n\nThe default is\n`metrics.prometheus.buckets = .005,.01,.025,.05,.1,.25,.5,1,2.5,5,10`\n"
  },
  {
    "path": "docs/content/ref/metrics.prometheus.path.md",
    "content": "---\ntitle: \"metrics.prometheus.path\"\n---\n\n`metrics.prometheus.path` configures the path to serve up metrics on any configured\n[proxy.addr](/ref/proxy.addr/) where `proto=prometheus`.\n\nDefaults to `/metrics`\n"
  },
  {
    "path": "docs/content/ref/metrics.prometheus.subsystem.md",
    "content": "---\ntitle: \"metrics.prometheus.subsystem\"\n---\n\n`metrics.prometheus.subsystem` configures the subsystem name when reporting\nmetrics.  This is basically appended to the prefix for metric names.\n\nSee https://prometheus.io/docs/practices/instrumentation/#subsystems\nfor more information.\n"
  },
  {
    "path": "docs/content/ref/metrics.retry.md",
    "content": "---\ntitle: \"metrics.retry\"\n---\n\n`metrics.retry` configures the interval with which fabio tries to\nconnect to the metrics backend during startup.\n\nThe default is\n\n\tmetrics.retry = 500ms\n"
  },
  {
    "path": "docs/content/ref/metrics.statsd.addr.md",
    "content": "---\ntitle: \"metrics.statsd.addr\"\n---\n\n`metrics.statsd.addr` configures the host:port of the StatsD\nserver. \n\nThis is required when [metrics.target](/ref/metrics.target/) is set to `statsd_raw`.\n\nThe default is\n\n\tmetrics.statsd.addr =\n"
  },
  {
    "path": "docs/content/ref/metrics.target.md",
    "content": "---\ntitle: \"metrics.target\"\n---\n\n`metrics.target` configures the backend the metrics values are sent to.\n\nPossible values are:\n\n* `<empty>`:  do not report metrics\n* `stdout`:   report metrics to stdout\n* `graphite`: report metrics to Graphite on [metrics.graphite.addr](/ref/metrics.graphite.addr/)\n* `statsd`: legacy statsd support, used in v1.5.5 and lower - removed in v1.6\n* `statsd_raw`: report metrics to StatsD on [metrics.statsd.addr](/ref/metrics.statsd.addr/) - this was \n  intentionally renamed because anyone upgrading to 1.6 will need to revisit their configuration anyway due to \n  rewrite of this backend.  It was quite broken before, the counters never reset, it did not follow the spec so the info was \n  likely wrong or people using this were doing some workarounds they'll need to remove anyway.\n* `circonus`: report metrics to Circonus (https://circonus.com/)\n* `prometheus`: use prometheus metrics. (https://prometheus.io)  Must be used in conjuction with a prometheus \n  listener in [proxy.addr](/ref/proxy.addr/)\n* `dogstatsd`: use with datadog dogstatsd (https://www.datadoghq.com/)\n\nThe default is\n\n\tmetrics.target =\n\nMultiple metrics targets can be defined separated by comma.\n"
  },
  {
    "path": "docs/content/ref/metrics.timeout.md",
    "content": "---\ntitle: \"metrics.timeout\"\n---\n\n`metrics.timeout` configures how long fabio tries to connect to the metrics\nbackend during startup.\n\nThe default is\n\n\tmetrics.timeout = 10s\n"
  },
  {
    "path": "docs/content/ref/proxy.addr.md",
    "content": "---\ntitle: \"proxy.addr\"\n---\n\n\n`proxy.addr` configures listeners.\n\nEach listener is configured with and address and a\nlist of optional arguments in the form of\n\n    [host]:port;opt=arg;opt[=arg];...\n\nEach listener has a protocol which is configured\nwith the `proto` option for which it routes and\nforwards traffic.\n\nThe supported protocols are:\n\n* `http` for HTTP based protocols\n* `https` for HTTPS based protocols\n* `grpc` for GRPC based protocols\n* `grpcs` for GRPC+TLS based protocols\n* `tcp` for a raw TCP proxy with or witout TLS support\n* `tcp+sni` for an SNI aware TCP proxy\n* `tcp-dynamic` for a consul driven TCP proxy\n* `https+tcp+sni` for an SNI aware TCP proxy with https fallthrough\n* `prometheus` for a prometheus metrics endpoint.  Used in conjunction with [metrics.target](/ref/metrics.target/)\n  =prometheus\n\nIf no `proto` option is specified then the protocol\nis either `http` or `https` depending on whether a\ncertificate source is configured via the `cs` option\nwhich contains the name of the certificate source.\n\nThe TCP+SNI proxy analyzes the `ClientHello` message\nof TLS connections to extract the server name\nextension and then forwards the encrypted traffic\nto the destination without decrypting the traffic.\n\n#### General options\n\n* `rt`: Sets the read timeout as a duration value (e.g. `3s`)\n\n* `wt`: Sets the write timeout as a duration value (e.g. `3s`)\n\n* `it`: Sets the idle timeout as a duration value (e.g. `3s`)\n\n* `strictmatch`: When set to `true` the certificate source must provide\n  a certificate that matches the hostname for the connection\n  to be established. Otherwise, the first certificate is used\n  if no matching certificate was found. This matches the default\n  behavior of the Go TLS server implementation.\n\n* `pxyproto`: When set to 'true' the listener will respect upstream v1\n  PROXY protocol headers.\n  NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10.\n  This changed to off when this option was introduced with\n  the 1.5.11 release.\n  For more information about the PROXY protocol, please see:\n  http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt\n\n* `pxytimeout`: Sets PROXY protocol header read timeout as a duration (e.g. '250ms').\n  This defaults to 250ms if not set when `pxyproto` is enabled.\n* `refresh`: Sets the refresh interval to check the route table for updates. Used when `tcp-dynamic` is enabled.\n#### TLS options\n\n* `tlsmin`: Sets the minimum TLS version for the handshake. This value\n  is one of `ssl30`, `tls10`, `tls11`, `tls12` or the corresponding\n  version number from https://golang.org/pkg/crypto/tls/#pkg-constants\n\n* `tlsmax`: Sets the maximum TLS version for the handshake. See `tlsmin`\n  for the format.\n\n* `tlsciphers`: Sets the list of allowed ciphers for the handshake. The value\n  is a quoted comma-separated list of the hex cipher values or\n  the constant names from https://golang.org/pkg/crypto/tls/#pkg-constants,\n  e.g. `\"0xc00a,0xc02b\"` or `\"TLS_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_128_CBC_SHA\"`\n\n#### Examples\n\n    # HTTP listener on port 9999\n    proxy.addr = :9999\n\n    # HTTP listener on IPv4 with read timeout\n    proxy.addr = 1.2.3.4:9999;rt=3s\n\n    # HTTP listener on IPv6 with write timeout\n    proxy.addr = [2001:DB8::A/32]:9999;wt=5s\n\n    # Multiple listeners\n    proxy.addr = 1.2.3.4:9999;rt=3s,[2001:DB8::A/32]:9999;wt=5s\n\n    # Multiple listeners with different protocols and options\n    proxy.addr = 172.16.20.11:80;proto=http;rt=60s;wt=30s, \\\n                 172.16.20.11:443;proto=https;rt=60s;wt=30s;cs=all;tlsmin=10, \\\n                 172.16.20.11:8443;proto=tcp+sni\n\n    # HTTPS listener on port 443 with certificate source\n    proxy.addr = :443;cs=some-name\n\n    # HTTPS listener on port 443 with certificate source and TLS options\n    proxy.addr = :443;cs=some-name;tlsmin=tls10;tlsmax=tls11;tlsciphers=\"0xc00a,0xc02b\"\n    \n    # GRPC listener on port 8888 \n    proxy.addr = :8888;proto=grpc\n    \n    # GRPCS listener on port 8888 with certificate source\n    proxy.addr = :8888;proto=grpcs;cs=some-name\n\n    # TCP listener on port 1234 with port routing\n    proxy.addr = :1234;proto=tcp\n\n    # TCP listener on port 443 with SNI routing\n    proxy.addr = :443;proto=tcp+sni\n\n    # TCP listener on port 443 with SNI routing with HTTPS fallthrough\n    proxy.addr = :443;proto=https+tcp+sni;cs=some-name\n\n    # TCP listeners using consul for config with 5 second refresh interval\n    proxy.addr = 0.0.0.0:0;proto=tcp-dynamic;refresh=5s\n\nThe default is\n\n    proxy.addr = :9999\n"
  },
  {
    "path": "docs/content/ref/proxy.auth.md",
    "content": "---\ntitle: \"proxy.auth\"\n---\n\n`proxy.auth` configures one or more authorization schemes.\n\nEach authorization scheme is configured with a list of\nkey/value options. Each scheme must have a unique\nname which can then be referred to in a routing rule.\n\n    name=<name>;type=<type>;opt=arg;opt[=arg];...\n\nThe following types of authorization schemes are available:\n\n#### Basic\n\nThe basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits.\n\nThe `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero.\nNote: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored.\n\n    name=<name>;type=basic;file=<file>;realm=<realm>;refresh=<interval>\n\nSupported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd)\n\n#### Examples\n\n    # single basic auth scheme\n    name=mybasicauth;type=basic;file=p/creds.file;\n\n    # single basic auth scheme with refresh interval set to 30 seconds\n    name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s\n\n    # basic auth with multiple schemes\n    proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s,\n                 name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm\n\nThe default is\n\n    proxy.auth =\n"
  },
  {
    "path": "docs/content/ref/proxy.cs.md",
    "content": "---\ntitle: \"proxy.cs\"\n---\n\n`proxy.cs` configures one or more certificate sources.\n\nEach certificate source is configured with a list of\nkey/value options. Each source must have a unique\nname which can then be referred to in a listener\nconfiguration.\n\n    cs=<name>;type=<type>;opt=arg;opt[=arg];...\n\nAll certificates need to be provided in PEM format.\n\nThe following types of certificate sources are available:\n\n#### File\n\nThe `file` certificate source supports one certificate which is loaded at\nstartup and is cached until the service exits.\n\nThe `cert` option contains the path to the certificate file. The `key`\noption contains the path to the private key file. If the certificate file\ncontains both the certificate and the private key the `key` option can be\nomitted. The `clientca` option contains the path to one or more client\nauthentication certificates.\n\n    cs=<name>;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem\n\n#### Path\n\nThe `path` certificate source loads certificates from a directory in\nalphabetical order and refreshes them periodically.\n\nThe `cert` option provides the path to the TLS certificates and the\n`clientca` option provides the path to the certificates for client\nauthentication.\n\nTLS certificates are stored either in one or two files:\n\n    www.example.com.pem or www.example.com-{cert,key}.pem\n\nTLS certificates are loaded in alphabetical order and the first certificate\nis the default for clients which do not support SNI.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nThe default refresh interval is 3 seconds and cannot be lower than 1 second\nto prevent busy loops. To load the certificates only once and disable\nautomatic refreshing set `refresh` to zero.\n\n    cs=<name>;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s\n\n#### HTTP\n\nThe `http` certificate source loads certificates from an HTTP/HTTPS server.\n\nThe `cert` option provides a URL to a text file which contains all files\nthat should be loaded from this directory. The filenames follow the same\nrules as for the path source. The text file can be generated with:\n\n    ls -1 *.pem > list\n\nThe `clientca` option provides a URL for the client authentication\ncertificates analogous to the `cert` option.\n\nAuthentication credentials can be provided in the URL as request parameter,\nas basic authentication parameters or through a header.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nThe default refresh interval is 3 seconds and cannot be lower than 1 second\nto prevent busy loops. To load the certificates only once and disable\nautomatic refreshing set `refresh` to zero.\n\n    cs=<name>;type=http;cert=https://host.com/path/to/cert/list&token=123\n    cs=<name>;type=http;cert=https://user:pass@host.com/path/to/cert/list\n    cs=<name>;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234\n\n#### Consul\n\nThe `consul` certificate source loads certificates from Consul.\n\nThe `cert` option provides a KV store URL where the the TLS certificates are\nstored.\n\nThe `clientca` option provides a URL to a path in the KV store where the the\nclient authentication certificates are stored.\n\nThe filenames follow the same rules as for the path source.\n\nThe TLS certificates are updated automatically whenever the KV store\nchanges. The client authentication certificates cannot be updated\nautomatically since Go does not provide a mechanism for that yet.\n\n    cs=<name>;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123\n\n#### Vault\n\nThe `vault` certificate store uses HashiCorp Vault as the certificate\nstore.\n\nThe `cert` option provides the path to the TLS certificates and the\n`clientca` option provides the path to the certificates for client\nauthentication.\n\nThe `refresh` option can be set to specify the refresh interval for the TLS\ncertificates. Client authentication certificates cannot be refreshed since\nGo does not provide a mechanism for that yet.\n\nThe default refresh interval is 3 seconds and cannot be lower than 1 second\nto prevent busy loops. To load the certificates only once and disable\nautomatic refreshing set `refresh` to zero.\n\nThe path to vault must be provided in the VAULT_ADDR environment\nvariable. The token must be provided in the VAULT_TOKEN environment\nvariable.\n\n    cs=<name>;type=vault;cert=secret/fabio/certs\n\n#### Vault PKI\n\nThe `vault-pki` certificate store uses HashiCorp Vault's PKI backend to issue\ncertificates on-demand.\n\nThe `cert` option provides a PKI backend path for issuing certificates. The\n`clientca` option works in the same way as for the generic Vault source.\n\nThe `refresh` option determines how long before the expiration date\ncertificates are re-issued. Values smaller than one hour are silently changed\nto one hour, which is also the default.\n\n    cs=<name>;type=vault-pki;cert=pki/issue/example-dot-com;refresh=24h;clientca=secret/fabio/client-certs\n\nThis source will issue server certificates on-demand using the PKI backend\nand re-issue them 24 hours before they expire. The CA for client\nauthentication is expected to be stored at secret/fabio/client-certs.\n\n#### Common options\n\nAll certificate stores support the following options:\n\n    caupgcn: Upgrade a self-signed client auth certificate with this common-name\n             to a CA certificate. Typically used for self-singed certificates\n             for the Amazon AWS Api Gateway certificates which do not have the\n             CA flag set which makes them unsuitable for client certificate\n             authentication in Go. For the AWS Api Gateway set this value\n             to `ApiGateway` to allow client certificate authentication.\n             This replaces the deprecated parameter `aws.apigw.cert.cn`\n             which was introduced in version 1.1.5.\n\n#### Examples\n\n    # file based certificate source\n    proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem\n\n    # path based certificate source\n    proxy.cs = cs=some-name;type=path;path=path/to/certs\n\n    # HTTP certificate source\n    proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs\n\n    # Consul certificate source\n    proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123\n\n    # Vault certificate source\n    proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs\n\n    # Vault PKI certificate source\n    proxy.cs = cs=some-name;type=vault-pki;cert=pki/issue/example-dot-com\n\n    # Multiple certificate sources\n    proxy.cs = cs=srcA;type=path;path=path/to/certs,\\\n               cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs\n\n    # path based certificate source for AWS Api Gateway\n    proxy.cs = cs=some-name;type=path;path=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway\n\nThe default is\n\n\tproxy.cs =\n\n"
  },
  {
    "path": "docs/content/ref/proxy.deregistergraceperiod.md",
    "content": "---\ntitle: \"proxy.deregistergraceperiod\"\n---\n\n`proxy.deregistergraceperiod` configures the time to wait before \nshutting down the proxies de-registering from the service registry.\n\nAfter a signal is caught Fabio will immediately de-register from the\nservice registry and wait for `proxy.deregistergraceperiod` letting\nin-flight requests finish after which it will continue with shutting\ndown the proxy.\n\nThe default is\n\n    proxy.deregistergraceperiod = 0s\n"
  },
  {
    "path": "docs/content/ref/proxy.dialtimeout.md",
    "content": "---\ntitle: \"proxy.dialtimeout\"\n---\n\n`proxy.dialtimeout` configures the connection timeout for\noutgoing connections by setting the [Timeout](https://golang.org/pkg/net/#Dialer.Timeout)\nof the [net.Dialer](https://golang.org/pkg/net/#Dialer)\n\nThe default is\n\n    proxy.dialtimeout = 30s\n"
  },
  {
    "path": "docs/content/ref/proxy.flushinterval.md",
    "content": "---\ntitle: \"proxy.flushinterval\"\n---\n\n`proxy.flushinterval` configures periodic flushing of the\nresponse buffer for SSE (server-sent events) connections.\nThey are detected when the `Accept` header is\n`text/event-stream`.\n\nThe default is\n\n    proxy.flushinterval = 1s\n"
  },
  {
    "path": "docs/content/ref/proxy.globalflushinterval.md",
    "content": "---\ntitle: \"proxy.globalflushinterval\"\n---\n\n`proxy.globalflushinterval` configures periodic flushing of the\nresponse buffer for proxied non-SSE connections. By default it is disabled.\n\nThe default is\n\n    proxy.globalflushinterval = 0\n"
  },
  {
    "path": "docs/content/ref/proxy.grpcmaxrxmsgsize.md",
    "content": "---\ntitle: \"proxy.grpcmaxrxmsgsize\"\n---\n\n`proxy.grpcmaxrxmsgsize` configures the grpc max receive message size in bytes.  The default\nvalue is\n\n    proxy.grpcmaxrxmsgsize = 4194304\n\nwhich is 4MB\n"
  },
  {
    "path": "docs/content/ref/proxy.grpcmaxtxmsgsize.md",
    "content": "---\ntitle: \"proxy.grpcmaxtxmsgsize\"\n---\n\n`proxy.grpcmaxtxmsgsize` configures the grpc max transmit message size in bytes.  The default\nvalue is\n\n    proxy.grpcmaxtxmsgsize = 4194304\n\nwhich is 4MB\n"
  },
  {
    "path": "docs/content/ref/proxy.grpcshutdowntimeout.md",
    "content": "---\ntitle: \"proxy.grpcshutdowntimeout\"\n---\n\n`proxy.grpcshutdowntimeout` configures the amount of time fabio will wait to attempt\nto close the connection while waiting for grpc traffic to finish to a backend that's been\nderegistered.  The default value is\n\n    proxy.grpcshutdowntimeout = 2s\n"
  },
  {
    "path": "docs/content/ref/proxy.gzip.contenttype.md",
    "content": "---\ntitle: \"proxy.gzip.contenttype\"\n---\n\n`proxy.gzip.contenttype` configures which responses should be compressed.\n\nBy default, responses sent to the client are not compressed even if the\nclient accepts compressed responses by setting the 'Accept-Encoding: gzip'\nheader. By setting this value responses are compressed if the `Content-Type`\nheader of the response matches and the response is not already compressed.\nThe list of compressable content types is defined as a regular expression.\nThe regular expression must follow the rules outlined in https://golang.org/pkg/regexp.\n\nA typical example is\n\n    proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$\n\nThe default is\n\n    proxy.gzip.contenttype =\n"
  },
  {
    "path": "docs/content/ref/proxy.header.clientip.md",
    "content": "---\ntitle: \"proxy.header.clientip\"\n---\n\n`proxy.header.clientip` configures the header for the request ip.\n\nThe remote ip address is taken from [http.Request.RemoteAddr](https://golang.org/pkg/net/http/#Request.RemoteAddr).\n\nThe default is\n\n    proxy.header.clientip =\n\n"
  },
  {
    "path": "docs/content/ref/proxy.header.requestid.md",
    "content": "---\ntitle: \"proxy.header.requestid\"\n---\n\n`proxy.header.requestid` configures the header for the adding a unique request id.\nWhen set non-empty value the proxy will set this header on every request to the\nunique UUID value.\n\nThe default is\n\n    proxy.header.requestid =\n\n"
  },
  {
    "path": "docs/content/ref/proxy.header.sts.maxage.md",
    "content": "---\ntitle: \"proxy.header.sts.maxage\"\n---\n\n`proxy.header.sts.maxage` enables and configures the max-age of HSTS for TLS requests.\nWhen set greater than zero this enables the Strict-Transport-Security header\nand sets the max-age value in the header.\n\nThe default is\n\n    proxy.header.sts.maxage = 0\n"
  },
  {
    "path": "docs/content/ref/proxy.header.sts.preload.md",
    "content": "---\ntitle: \"proxy.header.sts.preload\"\n---\n\n`proxy.header.sts.preload` instructs HSTS to include the preload directive.\nWhen set to true, the 'preload' option will be added to the\nStrict-Transport-Security header.\n\nSending the preload directive from your site can have PERMANENT CONSEQUENCES\nand prevent users from accessing your site and any of its subdomains if you\nfind you need to switch back to HTTP. Please read the details at\n[https://hstspreload.org/#removal](https://hstspreload.org/#removal)\nbefore sending the header with \"preload\".\n\nThe default is\n\n    proxy.header.sts.preload = false\n"
  },
  {
    "path": "docs/content/ref/proxy.header.sts.subdomains.md",
    "content": "---\ntitle: \"proxy.header.sts.subdomains\"\n---\n\n`proxy.header.sts.subdomains` instructs HSTS to include subdomains.\nWhen set to true, the 'includeSubDomains' option will be added to\nthe Strict-Transport-Security header.\n\nThe default is\n\n    proxy.header.sts.subdomains = false\n"
  },
  {
    "path": "docs/content/ref/proxy.header.tls.md",
    "content": "---\ntitle: \"proxy.header.tls\"\n---\n\n`proxy.header.tls` configures the header to set for TLS connections.\n\nWhen set to a non-empty value the proxy will set this header on every\nTLS request to the value of [proxy.header.tls.value](/ref/proxy.header.tls.value/)\n\nThe default is\n\n    proxy.header.tls =\n"
  },
  {
    "path": "docs/content/ref/proxy.header.tls.value.md",
    "content": "---\ntitle: \"proxy.header.tls.value\"\n---\n\n`proxy.header.tls.value` configures the value to set the [proxy.header.tls](/ref/proxy.header.tls/) header to for TLS connections.\n\nThe default is\n\n    proxy.header.tls.value =\n"
  },
  {
    "path": "docs/content/ref/proxy.idleconntimeout.md",
    "content": "`proxy.idleconntimeout` configures idle connection timeout, which influences\nwhen to close keep-alive connections.\n\nThe default is\n\n    proxy.idleconntimeout     = 15s\n"
  },
  {
    "path": "docs/content/ref/proxy.keepalivetimeout.md",
    "content": "`proxy.keepalivetimeout` configures the keep-alive timeout.\n\nThis configures the KeepAliveTimeout of the network dialer.\n\nThe default is\n\n    proxy.keepalivetimeout     = 0s\n"
  },
  {
    "path": "docs/content/ref/proxy.localip.md",
    "content": "---\ntitle: \"proxy.localip\"\n---\n\n`proxy.localip` configures the ip address of the proxy which is added\nto the Header configured by [`proxy.header.clientip`](/ref/proxy.header.clientip/) and to the `Forwarded: by=` attribute.\n\nThe local non-loopback address is detected during startup\nbut can be overwritten with this property.\n\nThe default is\n\n    proxy.localip =\n"
  },
  {
    "path": "docs/content/ref/proxy.matcher.md",
    "content": "---\ntitle: \"proxy.matcher\"\n---\n\n\n`proxy.matcher` configures the path matching algorithm.\n\n* `prefix`: prefix matching\n* `iprefix`: case insensitive prefix matching\n* `glob`:  glob matching\n\nWhen `prefix` matching is enabled then the route path must be a\nprefix of the request URI, e.g. `/foo` matches `/foo`, `/foot` but\nnot `/fo`.\n\nWhen `glob` matching is enabled the route is evaluated according to\nglobbing rules provided by the Go [`path.Match`](https://golang.org/pkg/path/#Match)\nfunction.\n\nFor example, `/foo*` matches `/foo`, `/fool` and `/fools`. Also, `/foo/*/bar`\nmatches `/foo/x/bar`.\n\n`iprefix` matching is similar to `prefix`, except it uses a case insensitive comparison\n\nThe default is\n\n    proxy.matcher = prefix\n"
  },
  {
    "path": "docs/content/ref/proxy.maxconn.md",
    "content": "---\ntitle: \"proxy.maxconn\"\n---\n\n`proxy.maxconn` configures the maximum number of cached\nincoming and outgoing connections.\n\nThis configures the [MaxIdleConnsPerHost](https://golang.org/pkg/net/http/#Transport.MaxIdleConnsPerHost)\nof the [http.Transport](https://golang.org/pkg/net/http/#Transport).\n\nThe default is\n\n    proxy.maxconn = 10000\n\n"
  },
  {
    "path": "docs/content/ref/proxy.noroutestatus.md",
    "content": "---\ntitle: \"proxy.noroutestatus\"\n---\n\n`proxy.noroutestatus` configures the response code when no route was found.\n\nThe default is\n\n    proxy.noroutestatus = 404\n"
  },
  {
    "path": "docs/content/ref/proxy.responseheadertimeout.md",
    "content": "---\ntitle: \"proxy.responseheadertimeout\"\n---\n\n`proxy.responseheadertimeout` configures the [ResponseHeaderTimeout](https://golang.org/pkg/net/http/#Transport.ResponseHeaderTimeout) \nof the [http.Transport](https://golang.org/pkg/net/http/#Transport).\n\nThe default is\n\n    proxy.responseheadertimeout = 0s\n"
  },
  {
    "path": "docs/content/ref/proxy.shutdownwait.md",
    "content": "---\ntitle: \"proxy.shutdownwait\"\n---\n\n`proxy.shutdownwait` configures the time for a graceful shutdown.\n\nAfter a signal is caught the proxy will immediately suspend\nrouting traffic and respond with a `503 Service Unavailable`\nfor the duration of the given period.\n\nThe default is\n\n    proxy.shutdownwait = 0s\n"
  },
  {
    "path": "docs/content/ref/proxy.strategy.md",
    "content": "---\ntitle: \"proxy.strategy\"\n---\n\n`proxy.strategy` configures the load balancing strategy.\n\n* `rnd`: pseudo-random distribution\n  configures a pseudo-random distribution by using the microsecond\n  fraction of the time of the request.\n\n* `rr`:  round-robin distribution\n  configures a round-robin distribution.\n\nThe default is\n\n    proxy.strategy = rnd\n\n"
  },
  {
    "path": "docs/content/ref/registry.backend.md",
    "content": "---\ntitle: \"registry.backend\"\n---\n\n`registry.backend` configures which backend is used.\nSupported backends are: `consul`, `static`, `file`, `custom`. If custom is used fabio makes an api \ncall to a remote system expecting the below json response\n\n```json\n[\n {\n  \"cmd\": \"string\",\n  \"service\": \"string\",\n  \"src\": \"string\",\n  \"dst\": \"string\",\n  \"weight\": float,\n  \"tags\": [\"string\"],\n  \"opts\": {\"string\":\"string\"}\n }\n]\n```\n\n\nThe default is\n\n\tregistry.backend = consul\n\nShort description of the fields required for a custom backend\n\nTo configure routes Fabio uses a Config Language, specified [here](https://fabiolb.net/cfg/)\n\n- cmd - the command to add, remove or change weight of a route. For example `route add` to add a new route mapping.\n- service - the name that the service will show up in the UI.\n- src - usually the prefix that will be used in the routing table.\n- dst - the endpoint that will be used as the destination of the routing table. \n- weight - defines the weight of this path to perform routing. For example route A 90% and route B 10% for canary deployments.\n- tags - a list of tags, provide a way to filter routes, making it easier to do operations like bulk deletes `route del tags \"dev\"`. \n- opts - a KV map of the config language list of options. for example `proto` or `prefix`\n"
  },
  {
    "path": "docs/content/ref/registry.consul.addr.md",
    "content": "---\ntitle: \"registry.consul.addr\"\n---\n\n`registry.consul.addr` configures the address of the Consul agent to connect to.\n\nThe default is\n\n\tregistry.consul.addr = localhost:8500\n"
  },
  {
    "path": "docs/content/ref/registry.consul.checksRequired.md",
    "content": "---\ntitle: \"registry.consul.checksRequired\"\n---\n\n`registry.consul.checksRequired` configures how many health checks \nmust pass in order for fabio to consider a service available.\n\nPossible values are:\n\n* `one`: at least one health check must pass\n* `all`: all health checks must pass\n\nThe default is\n\n\tregistry.consul.checksRequired = one\n"
  },
  {
    "path": "docs/content/ref/registry.consul.kvpath.md",
    "content": "---\ntitle: \"registry.consul.kvpath\"\n---\n\n`registry.consul.kvpath` configures the KV path for manual routes.\n\nThe Consul KV path is watched for changes which get appended to\nthe routing table. This allows for manual overrides and weighted\nround-robin routes.\n\nAs of version 1.5.7 fabio will treat the kv path as a prefix and\ncombine the values of the key itself and all its subkeys in\nalphabetical order.\n\nTo see all updates you may want to set [`-log.routes.format`](/ref/log.routes.format/)\nto `all`.\n\nYou can modify the content of the routes with the `consul` tool or via\nthe [Consul API](https://www.consul.io/api/index.html):\n\n```\nconsul kv put fabio/config \"route add svc /maint http://5.6.7.8:5000\\nroute add svc / http://1.2.3.4:5000\\n\"\n\n# fabio >= 1.5.7 supports prefix match\nconsul kv put fabio/config/maint    \"route add svc /maint http://5.6.7.8:5000\"\nconsul kv put fabio/config/catchall \"route add svc / http://1.2.3.4:5000\"\n\nconsul kv delete fabio/config/maint\n```\n\nThe default is\n\n\tregistry.consul.kvpath = /fabio/config\n"
  },
  {
    "path": "docs/content/ref/registry.consul.namespace.md",
    "content": "---\ntitle: \"registry.consul.namespace\"\n---\n\n`registry.consul.namespace` configures the consul namespace in which fabio will register itself.\n\n Namespaces are a feature only available in the enterprise version of consul. In the open-source\n version or with an empty namespace option fabio will be registered in the default namespace. Only\n services running in the same consul namespace will be picked up by fabio.\n\nThe default is\n\n\tregistry.consul.namespace = \n"
  },
  {
    "path": "docs/content/ref/registry.consul.noroutehtmlpath.md",
    "content": "---\ntitle: \"registry.consul.noroutehtmlpath\"\n---\n\n`registry.consul.noroutehtmlpath` configures the KV path for the HTML page when no route was found.\n\nThe consul KV path is watched for changes.\n\nThe default is\n\n\tregistry.consul.noroutehtmlpath = /fabio/noroute.html\n"
  },
  {
    "path": "docs/content/ref/registry.consul.pollInterval.md",
    "content": "---\ntitle: \"registry.consul.pollInterval\"\n---\n\n\t\n`registry.consul.pollInterval` configures the poll interval\nfor route updates. If Poll interval is set to 0 the updates will\nbe disabled and fall back to blocking queries.  Other values can\nbe any time definition. e.g. `1s, 100ms`\n\n\nThe default is\n\n    registry.consul.pollInterval = 0\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.addr.md",
    "content": "---\ntitle: \"registry.consul.register.addr\"\n---\n\n`registry.consul.register.addr` configures the address for the service registration.\n\nFabio registers itself in consul with this `host:port` address.\nIt must point to the UI/API endpoint configured by [ui.addr](/ref/ui.addr/) and defaults to its\nvalue.\n\nThe default is\n\n\tregistry.consul.register.addr = :9998\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.checkInterval.md",
    "content": "---\ntitle: \"registry.consul.register.checkInterval\"\n---\n\n`registry.consul.register.checkInterval` configures the interval for the health check.\n\nFabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health\nand this value tells consul how often to check it.\n\nThe default is\n\n\tregistry.consul.register.checkInterval = 1s\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.checkTLSSkipVerify.md",
    "content": "---\ntitle: \"registry.consul.register.checkTLSSkipVerify\"\n---\n\n`registry.consul.register.checkTLSSkipVerify` configures TLS verification for the health check.\n\nFabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health\nand this value tells consul to skip TLS certificate validation for\nhttps checks.\n\nThe default is\n\n\tregistry.consul.register.checkTLSSkipVerify = false\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.checkTimeout.md",
    "content": "---\ntitle: \"registry.consul.register.checkTimeout\"\n---\n\n`registry.consul.register.checkTimeout` configures the timeout for the health check.\n\nFabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health\nand this value tells Consul how long to wait for a response.\n\nThe default is\n\n\tregistry.consul.register.checkTimeout = 3s\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.deregisterCriticalServiceAfter.md",
    "content": "---\ntitle: \"registry.consul.register.deregisterCriticalServiceAfter\"\n---\n\nThis option is deprecated and has no effect in versions after 1.5.11. Services\nare now always deregistered shortly after fabio exits for any reason.\n\nIn versions up to and including 1.5.11\n`registry.consul.register.deregisterCriticalServiceAfter` configures the\nduration after which registered services are removed from Consul after fabio\nexits abruptly (services are always deregistered immediately when fabio exits\nnormally).\n\nAt the time of this writing, Consul enforces a minimum value of one minute and runs\nits reaper process every thirty seconds. \n\nThe default for fabio <= 1.5.11 is\n\n\tregistry.consul.register.deregisterCriticalServiceAfter = 90m\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.enabled.md",
    "content": "---\ntitle: \"registry.consul.register.enabled\"\n---\n\n`registry.consul.register.enabled` configures whether fabio registers itself in Consul.\n\nFabio will register itself in consul only if this value is set to `true` which\nis the default. To disable registration set it to any other value, e.g. `false`\n\nThe default is\n\n\tregistry.consul.register.enabled = true\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.name.md",
    "content": "---\ntitle: \"registry.consul.register.name\"\n---\n\n`registry.consul.register.name` configures the name for the service registration.\n\nFabio registers itself in consul under this service name.\n\nThe default is\n\n\tregistry.consul.register.name = fabio\n"
  },
  {
    "path": "docs/content/ref/registry.consul.register.tags.md",
    "content": "---\ntitle: \"registry.consul.register.tags\"\n---\n\n`registry.consul.register.tags` configures the tags for the service registration.\n\nFabio registers itself with these tags. You can provide a comma separated list of tags.\n\nThe default is\n\n\tregistry.consul.register.tags =\n"
  },
  {
    "path": "docs/content/ref/registry.consul.service.status.md",
    "content": "---\ntitle: \"registry.consul.service.status\"\n---\n\n`registry.consul.service.status` configures the valid service status\nvalues for services included in the routing table.\n\nThe values are a comma separated list of\n`passing`, `warning`, `critical` and `unknown`\n\nThe default is\n\n\tregistry.consul.service.status = passing\n"
  },
  {
    "path": "docs/content/ref/registry.consul.serviceMonitors.md",
    "content": "---\ntitle: \"registry.consul.serviceMonitors\"\n---\n\n`registry.consul.serviceMonitors` configures the concurrency for\nroute updates. Fabio will make up to the configured number of\nconcurrent calls to Consul to fetch status data for route\nupdates.\n\nThe default is\n\n\tregistry.consul.serviceMonitors = 1\n"
  },
  {
    "path": "docs/content/ref/registry.consul.tagprefix.md",
    "content": "---\ntitle: \"registry.consul.tagprefix\"\n---\n\n`registry.consul.tagprefix` configures the prefix for tags which define routes.\n\nServices which define routes publish one or more tags with host/path\nroutes which they serve. These tags must have this prefix to be\nrecognized as routes.\n\nThe default is\n\n\tregistry.consul.tagprefix = urlprefix-\n"
  },
  {
    "path": "docs/content/ref/registry.consul.token.md",
    "content": "---\ntitle: \"registry.consul.token\"\n---\n\n`registry.consul.token` configures the acl token for consul.\n\nThe default is\n\n\tregistry.consul.token =\n"
  },
  {
    "path": "docs/content/ref/registry.custom.checkTLSSkipVerify.md",
    "content": "---\ntitle: \"registry.custom.checkTLSSkipVerify\"\n---\n\n`registry.custom.checkTLSSkipVerify` disables the TLS validation for the API call\n\nThe default is\n\n    registry.custom.checkTLSSkipVerify = false"
  },
  {
    "path": "docs/content/ref/registry.custom.host.md",
    "content": "---\ntitle: \"registry.custom.host\"\n---\n\n`registry.custom.host` configures the host:port for fabio to make the API call\n\nThe default is\n\n    registry.custom.host ="
  },
  {
    "path": "docs/content/ref/registry.custom.path.md",
    "content": "---\ntitle: \"registry.custom.path\"\n---\n\n `registry.custom.path` is the path used in the custom back end API Call\n\n The path does not need to contain the initial '/'\n\n Example:\n\n     registry.custom.path = api/v1/\n\n The default is\n\n     registry.custom.path ="
  },
  {
    "path": "docs/content/ref/registry.custom.pollinginterval.md",
    "content": "---\ntitle: \"registry.custom.pollinterval\"\n---\n\n`registry.custom.pollinterval` is the length of time between API calls\n\nThe default is\n\n    registry.custom.pollinterval = 10s"
  },
  {
    "path": "docs/content/ref/registry.custom.queryparams.md",
    "content": "---\ntitle: \"registry.custom.queryparams\"\n---\n                                     \n `registry.custom.queryparams` is the query parameters used in the custom back\n end API Call\n\n Multiple query parameters should be separated with an &\n\n Example:\n\n     registry.custom.queryparams = foo=bar&bar=foo\n\n The default is\n\n    registry.custom.queryparams ="
  },
  {
    "path": "docs/content/ref/registry.custom.scheme.md",
    "content": "---\ntitle: \"registry.custom.scheme\"\n---\n\n`registry.custom.scheme` configures the scheme use to make the API call \nmust be one of `http`, `https`\n\nThe default is\n\n    registry.custom.scheme = https"
  },
  {
    "path": "docs/content/ref/registry.custom.timeout.md",
    "content": "---\ntitle: \"registry.custom.timeout\"\n---\n\n`registry.custom.timeout` controls the timeout for the API call\n\nThe default is\n\n    registry.custom.timeout = 5s"
  },
  {
    "path": "docs/content/ref/registry.file.noroutehtmlpath.md",
    "content": "---\ntitle: \"registry.file.noroutehtmlpath\"\n---\n\n`registry.file.noroutehtmlpath` configures the path the HTML page when no route was found.\n\nThe default is\n\n\tregistry.file.noroutehtmlpath =\n"
  },
  {
    "path": "docs/content/ref/registry.file.path.md",
    "content": "---\ntitle: \"registry.file.path\"\n---\n\n`registry.file.path` configures a file based routing table.\nThe value configures the path to the file with the routing table.\n\nThe default is\n\n\tregistry.file.path =\n"
  },
  {
    "path": "docs/content/ref/registry.retry.md",
    "content": "---\ntitle: \"registry.retry\"\n---\n\n`registry.retry` configures the interval with which fabio tries to\nconnect to the registry during startup.\n\nThe default is\n\n\tregistry.retry = 500ms\n"
  },
  {
    "path": "docs/content/ref/registry.static.noroutehtmlpath.md",
    "content": "---\ntitle: \"registry.static.noroutehtml\"\n---\n\n`registry.static.noroutehtml` configures the the HTML for the page when no route was found.\n\nThe default is\n\n\tregistry.static.noroutehtmlpath =\n"
  },
  {
    "path": "docs/content/ref/registry.static.routes.md",
    "content": "---\ntitle: \"registry.static.routes\"\n---\n\n`registry.static.routes` configures a static routing table.\n\n#### Example\n\n\tregistry.static.routes = \\\n\t\troute add svc / http://1.2.3.4:5000/\n\nThe default is\n\n\tregistry.static.routes =\n"
  },
  {
    "path": "docs/content/ref/registry.timeout.md",
    "content": "---\ntitle: \"registry.timeout\"\n---\n\n`registry.timeout` configures how long fabio tries to connect to the registry\nbackend during startup.\n\nThe default is\n\n\tregistry.timeout = 10s\n"
  },
  {
    "path": "docs/content/ref/runtime.gogc.md",
    "content": "---\ntitle: \"runtime.gogc\"\n---\n\n`runtime.gogc` configures GOGC (the GC target percentage).\n\nSetting `runtime.gogc` is equivalent to setting the `GOGC`\nenvironment variable which also takes precedence over\nthe value from the config file.\n\nIncreasing this value means fewer but longer GC cycles\nsince there is more garbage to collect.\n\nNOTE - the default for fabio up to 1.5.14 was 800.  This changed\nto 100 in version 1.5.15\n\nThe default is\n\n\truntime.gogc = 100\n"
  },
  {
    "path": "docs/content/ref/runtime.gomaxprocs.md",
    "content": "---\ntitle: \"runtime.gomaxprocs\"\n---\n\n`runtime.gomaxprocs` configures GOMAXPROCS.\n\nSetting `runtime.gomaxprocs` is equivalent to setting the `GOMAXPROCS`\nenvironment variable which also takes precedence over\nthe value from the config file.\n\nIf `runtime.gomaxprocs` < 0 then all CPU cores are used.\n\nThe default is\n\n\truntime.gomaxprocs = -1\n"
  },
  {
    "path": "docs/content/ref/ui.access.md",
    "content": "---\ntitle: \"ui.access\"\n---\n\n`ui.access` configures the access mode for the UI.\n\n* `ro`:  read-only access\n* `rw`:  read-write access\n\nThe default is\n\n\tui.access = rw\n"
  },
  {
    "path": "docs/content/ref/ui.addr.md",
    "content": "---\ntitle: \"ui.addr\"\n---\n\n`ui.addr` configures the address the UI is listening on.\n\nThe listener uses the same syntax as [proxy.addr](/ref/proxy.addr/) but\nsupports only a single listener. \n\nTo enable HTTPS configure a certificate source. You should use a different\ncertificate source than the one you use for the external connections, e.g.\n`cs=ui`.\n\nThe default is\n\n\tui.addr = :9998\n"
  },
  {
    "path": "docs/content/ref/ui.color.md",
    "content": "---\ntitle: \"ui.color\"\n---\n\n`ui.color` configures the background color of the UI.\nColor names are from http://materializecss.com/color.html\n\nThe default is\n\n\tui.color = light-green\n"
  },
  {
    "path": "docs/content/ref/ui.routingtable.source.host.md",
    "content": "---\ntitle: \"ui.routingtable.source.host\"\n---\n\n`ui.routingtable.source.host` configures an optional host or base address for the link in the source column.\n\nThis is only used when the source is not a separate server (does not begin with '/', e.g. 'dev.google.net'). If source is subdirectory it will set the link for the source to this host.\nIf this is not set, and the source link is enabled, the link will default to current host.  \n\nThis is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true.\n\nThe default is\n\n    ui.routingtable.source.host =\n"
  },
  {
    "path": "docs/content/ref/ui.routingtable.source.linkenabled.md",
    "content": "---\ntitle: \"ui.routingtable.source.linkenabled\"\n---\n\n`ui.routingtable.source.linkenabled` optionally configures if the routing table's column \"source\" should be a clickable link.  \n\nThe default is\n\n    ui.routingtable.source.linkenabled = false\n"
  },
  {
    "path": "docs/content/ref/ui.routingtable.source.newtab.md",
    "content": "---\ntitle: \"ui.routingtable.source.newtab\"\n---\n\n`ui.routingtable.source.newtab` configures if the source link should open in a new tab.\n\nThis is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true.\n\nThe default is\n\n    ui.routingtable.source.newtab = true\n"
  },
  {
    "path": "docs/content/ref/ui.routingtable.source.port.md",
    "content": "---\ntitle: \"ui.routingtable.source.port\"\n---\n\n`ui.routingtable.source.port` configures an optional port for the routing table source column link.  This is used in conjunction with the [scheme](/ref/ui.routingtable.source.scheme/) and [host](/ref/ui.routingtable.source.host/).  \n\nIf the source is not a separate server (does not begin with '/', e.g. 'dev.google.net'), and the [host](/ref/ui.routingtable.source.host/) is set, this will use the port that is set, or default to the current scheme protocol port (80 for http or 443 for https).  \n\nThis is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true.\n\nThe default is\n\n    ui.routingtable.source.port = \n"
  },
  {
    "path": "docs/content/ref/ui.routingtable.source.scheme.md",
    "content": "---\ntitle: \"ui.routingtable.source.scheme\"\n---\n\n`ui.routingtable.source.scheme` configures the scheme protocol for the link of the source on the routing table.  This is useful when the scheme is different than the current page or to force the traffic to a certain protocol.\n\n\nThis is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true.\n\nThe default is\n\n    ui.routingtable.source.scheme = http\n"
  },
  {
    "path": "docs/content/ref/ui.title.md",
    "content": "---\ntitle: \"ui.title\"\n---\n\n`ui.title` configures an optional title for the UI.\n\nThe default is\n\n\tui.title =\n"
  },
  {
    "path": "docs/layouts/404.html",
    "content": "{{partial \"header\" .}}\n\n<main class=\"container\">\n    <div class=\"row\">\n        {{partial \"sidebar\" .}}\n        <article class=\"col-sm-9 main-content\" role=\"main\">\n            <section>\n                <h2>Page Not Found</h2>\n                <a href=\"{{ \"/\" | relURL }}\">Return Home</a>\n            </section>\n        </article>\n    </div>\n</main>\n\n{{partial \"footer\" .}}\n"
  },
  {
    "path": "docs/layouts/_default/section.html",
    "content": "{{partial \"header\" .}}\n\n<main class=\"container\">\n  <div class=\"row\">\n    {{partial \"sidebar\" .}}\n\n    <!-- Main content -->\n    <article class=\"col-sm-9 main-content\" role=\"main\">\n\n      <section>\n        <h2>{{.Title}}</h2>\n        {{if .Params.since}}<p class=\"since\">{{.Params.since}}</p>{{end}}\n        {{.Content}}\n      </section>\n\n    </article>\n    <!-- END Main content -->\n\n  </div>\n</main>\n\n{{partial \"footer\" .}}\n"
  },
  {
    "path": "docs/layouts/_default/single.html",
    "content": "{{partial \"header\" .}}\n\n<main class=\"container\">\n  <div class=\"row\">\n    {{partial \"sidebar\" .}}\n\n    <!-- Main content -->\n    <article class=\"col-sm-9 main-content\" role=\"main\">\n\n      {{$section := .}}\n      <section>\n        <h2>{{.Title}}</h2>\n        {{.Content}}\n        {{range .Pages}}\n        <h3 id=\"{{printf \"%s %s\" $section.Title .Title | urlize}}\">{{.Title}} <a href=\"#top\">^</a></h3>\n          {{if .Params.since}}<p class=\"since\">{{.Params.since}}</p>{{end}}\n          {{.Content}}\n        {{end}}\n      </section>\n\n    </article>\n    <!-- END Main content -->\n\n  </div>\n</main>\n\n{{partial \"footer\" .}}\n"
  },
  {
    "path": "docs/layouts/index.html",
    "content": "{{partial \"header\" .}}\n\n<main class=\"container\">\n  <div class=\"row\">\n    {{partial \"sidebar\" .}}\n    <article class=\"col-sm-9 main-content\" role=\"main\">\n      <section>\n        <h2>{{.Title}}</h2>\n        {{.Content}}\n      </section>\n    </article>\n  </div>\n</main>\n\n{{partial \"footer\" .}}\n"
  },
  {
    "path": "docs/layouts/partials/footer.html",
    "content": "    <!-- Footer -->\n    <footer class=\"site-footer\">\n      <div class=\"container\">\n        <a id=\"scroll-up\" href=\"#\"><i class=\"fa fa-angle-up\"></i></a>\n\n        <div class=\"row\">\n          <div class=\"col-md-6 col-sm-6\">\n            <p style=\"line-height: 26px;\">\n                Copyright &copy; 2020-{{ now.Format \"2006\" }} Education Networks of America. All rights reserved.</br>\n                Copyright &copy; 2017-2020 Frank Schröder. All rights reserved.</br>\n                Copyright &copy; 2015-2017 eBay Software Foundation.  All rights reserved</br>\n                Last updated on {{ now.Format \"2 Jan 2006 15:04\"}}\n            </p>\n          </div>\n          <div class=\"col-md-6 col-sm-6\">\n            <ul class=\"footer-menu\">\n              <li><a href=\"https://github.com/fabiolb/fabio/blob/master/CHANGELOG.md\">Changelog</a></li>\n              <li><a href=\"/contact/\">Contact</a></li>\n              <li>\n                <a href=\"https://gohugo.io\"><img src=\"/img/made-with-hugo.png\" height=\"51px\"></a>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </footer>\n    <!-- END Footer -->\n\n    <!-- Global site tag (gtag.js) - Google Analytics -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id={{$.Site.Params.googleAnalytics}}\"></script>\n    <script>\n      window.dataLayer = window.dataLayer || [];\n      function gtag(){dataLayer.push(arguments);}\n      gtag('js', new Date());\n\n    gtag('config', '{{$.Site.Params.googleAnalytics}}');\n    </script>\n\n    <!-- Scripts -->\n    <script src=\"/js/theDocs.all.min.js\"></script>\n    <script src=\"/js/custom.js\"></script>\n\n  </body>\n</html>\n"
  },
  {
    "path": "docs/layouts/partials/header.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"keywords\" content=\"\">\n\n    <title>{{.Title}}</title>\n\n    <!-- Styles -->\n    <link href=\"/css/theDocs.all.min.css\" rel=\"stylesheet\">\n    <link href=\"/css/skin/skin-blue.min.css\" rel=\"stylesheet\">\n    <link href=\"/css/custom.css\" rel=\"stylesheet\">\n\n    <!-- Fonts -->\n    <!--<link href='http://fonts.googleapis.com/css?family=Raleway:100,300,400,500%7CLato:300,400' rel='stylesheet' type='text/css'>-->\n\n    <!-- Favicons -->\n    <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/img/apple-touch-icon.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/img/favicon-32x32.png\">\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/img/favicon-16x16.png\">\n    <link rel=\"manifest\" href=\"/img/manifest.json\">\n    <link rel=\"mask-icon\" href=\"/img/safari-pinned-tab.svg\" color=\"#5bbad5\">\n    <meta name=\"theme-color\" content=\"#ffffff\">\n  </head>\n\n  <body data-spy=\"scroll\" data-target=\".sidebar\" data-offset=\"400\">\n    <!-- top anchor -->\n    <a id=\"top\" href=\"#\"></a>\n\n    <header class=\"site-header sticky\">\n      <nav class=\"navbar navbar-default\">\n        <div class=\"container\">\n\n          <!-- Toggle buttons and brand -->\n          <div class=\"navbar-header\">\n            <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\"#navbar\" aria-expanded=\"true\" aria-controls=\"navbar\">\n              <span class=\"glyphicon glyphicon-option-vertical\"></span>\n            </button>\n\n            <button type=\"button\" class=\"navbar-toggle for-sidebar\" data-toggle=\"offcanvas\">\n              <span class=\"icon-bar\"></span>\n              <span class=\"icon-bar\"></span>\n              <span class=\"icon-bar\"></span>\n            </button>\n\n            <a class=\"navbar-brand\" href=\"/\"><img src=\"/img/logo-bw.svg\" alt=\"logo\"></a>\n          </div>\n          <!-- END Toggle buttons and brand -->\n\n          <!-- Top navbar -->\n          <div id=\"navbar\" class=\"navbar-collapse collapse\" aria-expanded=\"true\" role=\"banner\">\n            <ul class=\"nav navbar-nav navbar-right\">\n              <li><a href=\"/quickstart/\">Quickstart</a></li>\n              <li><a href=\"/deploy/\">Deployment</a></li>\n              <li><a href=\"/feature/\">Features</a></li>\n              <li><a href=\"/ref/\">Reference</a></li>\n              <li><a href=\"/faq/\">FAQ</a></li>\n              <li><a target=\"_blank\" href=\"https://twitter.com/search?q=%23fabiolb\"><i class=\"fa fa-twitter fa-2x\" aria-hidden=\"true\"></i></a></li>\n              <li><a target=\"_blank\" href=\"https://github.com/fabiolb/fabio\"><i class=\"fa fa-github fa-2x\" aria-hidden=\"true\"></i></a></li>\n            </ul>\n          </div>\n          <!-- END Top navbar -->\n\n        </div>\n      </nav>\n\n    </header>\n\n"
  },
  {
    "path": "docs/layouts/partials/sidebar.html",
    "content": "{{$currentPage := .}}\n<aside class=\"col-sm-3 sidebar sidebar-line\">\n\n  <ul class=\"nav sidenav dropable \">\n    {{range .Site.Sections}}\n    {{$section := .}}\n    <li>\n      <a class=\"{{if eq $currentPage.Permalink .Permalink}} active{{end}}\" href=\"{{.Permalink}}\">{{.Title}}</a>\n      {{if and .Pages (hasPrefix $currentPage.Permalink $section.Permalink)}}\n      <ul class=\"open\">\n      {{range .Pages}}\n        <li><a{{if eq $currentPage.Permalink .Permalink}} class=\"active\"{{end}} href=\"{{.Permalink}}\">{{.Title}}</a></li>\n      {{end}}\n      </ul>\n      {{end}}\n    </li>\n    {{end}}\n  </ul>\n\n</aside>\n"
  },
  {
    "path": "docs/static/CNAME",
    "content": "fabiolb.net\n"
  },
  {
    "path": "docs/static/check/ok",
    "content": "ok"
  },
  {
    "path": "docs/static/css/custom.css",
    "content": "/* workaround for firefox to display the logo */\n.site-header .navbar-brand > img {\n\theight: 100%;\n}\n\n/* set navbar to static for now. If JS is enabled, we will revert to fixed in\n * custom.js, but if JS isn't enabled a fixed header overlaps the content. */\n.site-header.sticky .navbar {\n    position: static;\n}\n\narticle.main-content {\n\tpadding-top: 0;\n}\n\nbody, h1,h2,h3,h4,h5,h6 {\n\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n}\n\nh2 a:after {\n\tbackground-color: transparent;\n}\n\n.banner.auto-size {\n\tbackground-color: #62a8ea;\n\tpadding-top: 40px;\n}\n\np.since::before {\n\tcontent: \"Since \";\n}\np.since {\n\tfont-size: 16px;\n}\n\n/* adjustment to code example sections so they are more readable */\npre[class*=\"language\"], pre code[class*=\"language\"] {\n\ttext-shadow: none;\n\tcolor: #808080;\n}\npre code .token.operator, pre code[class*=\"operator\"] {\n\tbackground: inherit;\n}\n.clipboard-copy {\n\ttop: 6px !important;\n\tright: 8px;\n\tcolor: #808080;\n\tbackground-color: transparent;\n\ttext-transform: uppercase;\n\tletter-spacing: 1px;\n\tfont-weight: 600;\n\topacity: 1;\n\tpadding: 2px 5px;\n}\npre .language-name {\n\tright: 75px;\n}\n"
  },
  {
    "path": "docs/static/img/manifest.json",
    "content": "{\n    \"name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-256x256.png\",\n            \"sizes\": \"256x256\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}"
  },
  {
    "path": "docs/static/js/custom.js",
    "content": "$(function() {\n  // Make the navbar only fixed if JS is enabled, else it overlaps the content.\n  $(\".site-header.sticky .navbar\").css('position', 'fixed');\n});\n"
  },
  {
    "path": "exit/listen.go",
    "content": "// Package exit allows to register callbacks which are called on program exit.\npackage exit\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n)\n\nvar wg sync.WaitGroup\n\n// quit channel is closed to cleanup exit listeners.\nvar quit = make(chan bool)\n\n// Listen registers an exit handler which is called on\n// SIGINT/SIGTERM or when Exit/Fatal/Fatalf is called.\n// SIGHUP is ignored since that is usually used for\n// triggering a reload of configuration which isn't\n// supported but shouldn't kill the process either.\nfunc Listen(fn func(os.Signal)) {\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor {\n\t\t\tsigchan := make(chan os.Signal, 1)\n\t\t\tsignal.Notify(sigchan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)\n\n\t\t\tvar sig os.Signal\n\t\t\tselect {\n\t\t\tcase sig = <-sigchan:\n\t\t\t\tswitch sig {\n\t\t\t\tcase syscall.SIGHUP:\n\t\t\t\t\tlog.Print(\"[INFO] Caught SIGHUP. Ignoring\")\n\t\t\t\t\tcontinue\n\t\t\t\tcase os.Interrupt:\n\t\t\t\t\tlog.Print(\"[INFO] Caught SIGINT. Exiting\")\n\t\t\t\tcase syscall.SIGTERM:\n\t\t\t\t\tlog.Print(\"[INFO] Caught SIGTERM. Exiting\")\n\t\t\t\tdefault:\n\t\t\t\t\t// fallthrough in case we forgot to add a switch clause.\n\t\t\t\t\tlog.Printf(\"[INFO] Caught signal %v. Exiting\", sig)\n\t\t\t\t}\n\t\t\tcase <-quit:\n\t\t\t}\n\t\t\tif fn != nil {\n\t\t\t\tfn(sig)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}()\n}\n\n// stubbed out for testing\nvar osExit = os.Exit\n\n// Exit terminates the application via os.Exit but\n// waits for all exit handlers to complete before\n// calling os.Exit.\nfunc Exit(code int) {\n\tdefer func() { recover() }() // don't panic if close(quit) is called concurrently\n\tclose(quit)\n\twg.Wait()\n\tosExit(code)\n}\n\n// Fatal is a replacement for log.Fatal which will trigger\n// the closure of all registered exit handlers and waits\n// for their completion and then call os.Exit(1).\nfunc Fatal(v ...interface{}) {\n\tlog.Print(v...)\n\tExit(1)\n}\n\n// Fatalf is a replacement for log.Fatalf and behaves like Fatal.\nfunc Fatalf(format string, v ...interface{}) {\n\tlog.Printf(format, v...)\n\tExit(1)\n}\n\n// Wait waits for all exit handlers to complete.\nfunc Wait() {\n\twg.Wait()\n}\n"
  },
  {
    "path": "exit/listen_test.go",
    "content": "package exit\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestExit(t *testing.T) {\n\tvar b bytes.Buffer\n\tvar exitCode int\n\tvar sig1, sig2 bool\n\n\tflags := log.Flags()\n\tlog.SetFlags(0)\n\tlog.SetOutput(&b)\n\tdefer func() {\n\t\tlog.SetOutput(os.Stderr)\n\t\tlog.SetFlags(flags)\n\t}()\n\n\tosExit = func(code int) { exitCode = code }\n\tdefer func() { osExit = os.Exit }()\n\n\tListen(func(os.Signal) { sig1 = true })\n\tListen(func(os.Signal) { sig2 = true })\n\n\t// trigger a concurrent exit via fatal/fatalf\n\t// it is not guaranteed that any log output is written\n\t// before the application exits. This is only to test\n\t// that two go routines can call Fatal without causing a\n\t// panic.\n\tgo Fatal(\"a\")\n\tgo Fatalf(\"b\")\n\n\t// wait for listeners to return\n\tWait()\n\n\tout := b.String()\n\tif !strings.Contains(\"a\\n b\\n a\\nb\\n b\\na\\n\", out) {\n\t\tt.Errorf(\"log.Fatal did not happen: %q\", out)\n\t}\n\tif exitCode != 1 {\n\t\tt.Errorf(\"os.Exit not called\")\n\t}\n\tif !sig1 || !sig2 {\n\t\tt.Errorf(\"signal handlers not completed\")\n\t}\n}\n"
  },
  {
    "path": "fabio.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"Go\" enabled=\"true\" />\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "fabio.properties",
    "content": "# proxy.cs configures one or more certificate sources.\n#\n# Each certificate source is configured with a list of\n# key/value options. Each source must have a unique\n# name which can then be referred to in a listener\n# configuration.\n#\n#   cs=<name>;type=<type>;opt=arg;opt[=arg];...\n#\n# All certificates need to be provided in PEM format.\n#\n# The following types of certificate sources are available:\n#\n# File\n#\n# The file certificate source supports one certificate which is loaded at\n# startup and is cached until the service exits.\n#\n# The 'cert' option contains the path to the certificate file. The 'key'\n# option contains the path to the private key file. If the certificate file\n# contains both the certificate and the private key the 'key' option can be\n# omitted. The 'clientca' option contains the path to one or more client\n# authentication certificates.\n#\n#   cs=<name>;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem\n#\n# Path\n#\n# The path certificate source loads certificates from a directory in\n# alphabetical order and refreshes them periodically.\n#\n# The 'cert' option provides the path to the TLS certificates and the\n# 'clientca' option provides the path to the certificates for client\n# authentication.\n#\n# TLS certificates are stored either in one or two files:\n#\n#   www.example.com.pem or www.example.com-{cert,key}.pem\n#\n# TLS certificates are loaded in alphabetical order and the first certificate\n# is the default for clients which do not support SNI.\n#\n# The 'refresh' option can be set to specify the refresh interval for the TLS\n# certificates. Client authentication certificates cannot be refreshed since\n# Go does not provide a mechanism for that yet.\n#\n# The default refresh interval is 3 seconds and cannot be lower than 1 second\n# to prevent busy loops. To load the certificates only once and disable\n# automatic refreshing set 'refresh' to zero.\n#\n#   cs=<name>;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s\n#\n# HTTP\n#\n# The http certificate source loads certificates from an HTTP/HTTPS server.\n#\n# The 'cert' option provides a URL to a text file which contains all files\n# that should be loaded from this directory. The filenames follow the same\n# rules as for the path source. The text file can be generated with:\n#\n#   ls -1 *.pem > list\n#\n# The 'clientca' option provides a URL for the client authentication\n# certificates analogous to the 'cert' option.\n#\n# Authentication credentials can be provided in the URL as request parameter,\n# as basic authentication parameters or through a header.\n#\n# The 'refresh' option can be set to specify the refresh interval for the TLS\n# certificates. Client authentication certificates cannot be refreshed since\n# Go does not provide a mechanism for that yet.\n#\n# The default refresh interval is 3 seconds and cannot be lower than 1 second\n# to prevent busy loops. To load the certificates only once and disable\n# automatic refreshing set 'refresh' to zero.\n#\n#   cs=<name>;type=http;cert=https://host.com/path/to/cert/list&token=123\n#   cs=<name>;type=http;cert=https://user:pass@host.com/path/to/cert/list\n#   cs=<name>;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234\n#\n# Consul\n#\n# The consul certificate source loads certificates from consul.\n#\n# The 'cert' option provides a KV store URL where the the TLS certificates are\n# stored.\n#\n# The 'clientca' option provides a URL to a path in the KV store where the the\n# client authentication certificates are stored.\n#\n# The filenames follow the same rules as for the path source.\n#\n# The TLS certificates are updated automatically whenever the KV store\n# changes. The client authentication certificates cannot be updated\n# automatically since Go does not provide a mechanism for that yet.\n#\n#   cs=<name>;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123\n#\n# Vault\n#\n# The Vault certificate store uses HashiCorp Vault as the certificate\n# store.\n#\n# The 'cert' option provides the path to the TLS certificates and the\n# 'clientca' option provides the path to the certificates for client\n# authentication.\n#\n# The 'refresh' option can be set to specify the refresh interval for the TLS\n# certificates. Client authentication certificates cannot be refreshed since\n# Go does not provide a mechanism for that yet.\n#\n# The default refresh interval is 3 seconds and cannot be lower than 1 second\n# to prevent busy loops. To load the certificates only once and disable\n# automatic refreshing set 'refresh' to zero.\n#\n# The path to vault must be provided in the VAULT_ADDR environment\n# variable. The token can be provided in the VAULT_TOKEN environment\n# variable, or provided by using the Vault fetch token option.  By default the\n# token is loaded once from the VAULT_TOKEN environment variable.  See Vault PKI for details.\n#\n#   cs=<name>;type=vault;cert=secret/fabio/certs\n#\n# Vault PKI\n#\n# The Vault PKI certificate store uses HashiCorp Vault's PKI backend to issue\n# certificates on-demand.\n#\n# The 'cert' option provides a PKI backend path for issuing certificates. The\n# 'clientca' option works in the same way as for the generic Vault source.\n#\n# The 'refresh' option determines how long before the expiration date\n# certificates are re-issued. Values smaller than one hour are silently changed\n# to one hour, which is also the default.\n#\n#   cs=<name>;type=vault-pki;cert=pki/issue/example-dot-com;refresh=24h;clientca=secret/fabio/client-certs\n#\n# This source will issue server certificates on-demand using the PKI backend\n# and re-issue them 24 hours before they expire. The CA for client\n# authentication is expected to be stored at secret/fabio/client-certs.\n#\n# 'vaultfetchtoken' enables fetching the vault token from a file on the filesystem or an environment\n# variable at the Vault refresh interval.  If fetching the token from a file the 'file:[path]' syntax should be used,\n# if fetching the token from an env variable, the 'env:[ENV]' syntax should be used.\n#\n#  cs=<name>;type=vault;cert=secret/fabio/certs;vaultfetchtoken=env:VAULT_TOKEN\n#\n# Common options\n#\n# All certificate stores support the following options:\n#\n#   caupgcn: Upgrade a self-signed client auth certificate with this common-name\n#            to a CA certificate. Typically used for self-singed certificates\n#            for the Amazon AWS Api Gateway certificates which do not have the\n#            CA flag set which makes them unsuitable for client certificate\n#            authentication in Go. For the AWS Api Gateway set this value\n#            to 'ApiGateway' to allow client certificate authentication.\n#            This replaces the deprecated parameter 'aws.apigw.cert.cn'\n#            which was introduced in version 1.1.5.\n#\n# Examples:\n#\n#     # file based certificate source\n#     proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem\n#\n#     # path based certificate source\n#     proxy.cs = cs=some-name;type=path;path=path/to/certs\n#\n#     # HTTP certificate source\n#     proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs\n#\n#     # Consul certificate source\n#     proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123\n#\n#     # Vault certificate source\n#     proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs\n#\n#     # Vault PKI certificate source\n#     proxy.cs = cs=some-name;type=vault-pki;cert=pki/issue/example-dot-com\n#\n#     # Multiple certificate sources\n#     proxy.cs = cs=srcA;type=path;path=path/to/certs,\\\n#                cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs\n#\n#     # path based certificate source for AWS Api Gateway\n#     proxy.cs = cs=some-name;type=path;path=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway\n#\n# The default is\n#\n# proxy.cs =\n\n\n# proxy.addr configures listeners.\n#\n# Each listener is configured with and address and a\n# list of optional arguments in the form of\n#\n#   [host]:port;opt=arg;opt[=arg];...\n#\n# Each listener has a protocol which is configured\n# with the 'proto' option for which it routes and\n# forwards traffic.\n#\n# The supported protocols are:\n#\n#   * http for HTTP based protocols\n#   * https for HTTPS based protocols\n#   * tcp for a raw TCP proxy with or witout TLS support\n#   * tcp+sni for an SNI aware TCP proxy\n#   * tcp-dynamic for a consul driven TCP proxy\n#   * https+tcp+sni for an SNI aware TCP proxy with https fallthrough\n#   * prometheus for a prometheus listener.  use this with the prometheus metrics target.\n#\n# If no 'proto' option is specified then the protocol\n# is either 'http' or 'https' depending on whether a\n# certificate source is configured via the 'cs' option\n# which contains the name of the certificate source.\n#\n# The TCP+SNI proxy analyzes the ClientHello message\n# of TLS connections to extract the server name\n# extension and then forwards the encrypted traffic\n# to the destination without decrypting the traffic.\n#\n# General options:\n#\n#   rt:          Sets the read timeout as a duration value (e.g. '3s')\n#\n#   wt:          Sets the write timeout as a duration value (e.g. '3s')\n#\n#   it:          Sets the idle timeout as a duration value (e.g. '3s')\n#\n#   strictmatch: When set to 'true' the certificate source must provide\n#                a certificate that matches the hostname for the connection\n#                to be established. Otherwise, the first certificate is used\n#                if no matching certificate was found. This matches the default\n#                behavior of the Go TLS server implementation.\n#\n#   pxyproto:    When set to 'true' the listener will respect upstream v1\n#                PROXY protocol headers.\n#                NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10.\n#                This changed to off when this option was introduced with\n#                the 1.5.11 release.\n#                For more information about the PROXY protocol, please see:\n#                http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt\n#\n#   pxytimeout:  Sets PROXY protocol header read timeout as a duration (e.g. '250ms').\n#                This defaults to 250ms if not set when 'pxyproto' is enabled.\n#\n#   refresh:     Sets the refresh interval to check the route table for updates.\n#                Used when 'tcp-dynamic' is enabled.\n#\n# TLS options:\n#\n#   tlsmin:      Sets the minimum TLS version for the handshake. This value\n#                is one of [ssl30, tls10, tls11, tls12] or the corresponding\n#                version number from https://golang.org/pkg/crypto/tls/#pkg-constants\n#\n#   tlsmax:      Sets the maximum TLS version for the handshake. See 'tlsmin'\n#                for the format.\n#\n#   tlsciphers:  Sets the list of allowed ciphers for the handshake. The value\n#                is a quoted comma-separated list of the hex cipher values or\n#                the constant names from https://golang.org/pkg/crypto/tls/#pkg-constants,\n#                e.g. \"0xc00a,0xc02b\" or \"TLS_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_128_CBC_SHA\"\n#\n# Examples:\n#\n#     # HTTP listener on port 9999\n#     proxy.addr = :9999\n#\n#     # HTTP listener on IPv4 with read timeout\n#     proxy.addr = 1.2.3.4:9999;rt=3s\n#\n#     # HTTP listener on IPv6 with write timeout\n#     proxy.addr = [2001:DB8::A/32]:9999;wt=5s\n#\n#     # Multiple listeners\n#     proxy.addr = 1.2.3.4:9999;rt=3s,[2001:DB8::A/32]:9999;wt=5s\n#\n#     # HTTPS listener on port 443 with certificate source\n#     proxy.addr = :443;cs=some-name\n#\n#     # HTTPS listener on port 443 with certificate source and TLS options\n#     proxy.addr = :443;cs=some-name;tlsmin=tls10;tlsmax=tls11;tlsciphers=\"0xc00a,0xc02b\"\n#\n#     # TCP listener on port 1234 with port routing\n#     proxy.addr = :1234;proto=tcp\n#\n#     # TCP listener on port 443 with SNI routing\n#     proxy.addr = :443;proto=tcp+sni\n#\n#     # TCP listener on port 443 with SNI routing with HTTPS fallthrough\n#     proxy.addr = :443;proto=https+tcp+sni;cs=some-name\n#\n#     # TCP listeners using consul for config with 5 second refresh interval\n#     proxy.addr = 0.0.0.0:0;proto=tcp-dynamic;refresh=5s\n#\n#     # prometheus listener.  can optionally be used with cs= as well for TLS support.\n#     proxy.addr = :9090;proto=prometheus;cs=some-name\n#\n# The default is\n#\n# proxy.addr = :9999\n\n\n# proxy.localip configures the ip address of the proxy which is added\n# to the Header configured by proxy.header.clientip and to the 'Forwarded: by=' attribute.\n#\n# The local non-loopback address is detected during startup\n# but can be overwritten with this property.\n#\n# The default is\n#\n# proxy.localip =\n\n\n# proxy.strategy configures the load balancing strategy.\n#\n# rnd: pseudo-random distribution\n# rr:  round-robin distribution\n#\n# \"rnd\" configures a pseudo-random distribution by using the microsecond\n# fraction of the time of the request.\n#\n# \"rr\" configures a round-robin distribution.\n#\n# The default is\n#\n# proxy.strategy = rnd\n\n\n# proxy.matcher configures the path matching algorithm.\n#\n# prefix: prefix matching\n# glob:  glob matching\n# iprefix: case-insensitive prefix matching\n#\n# The default is\n#\n# proxy.matcher = prefix\n\n\n# proxy.noroutestatus configures the response code when no route was found.\n#\n# The default is\n#\n# proxy.noroutestatus = 404\n\n\n# proxy.shutdownwait configures the time for a graceful shutdown.\n#\n# After a signal is caught the proxy will immediately suspend\n# routing traffic and respond with a 503 Service Unavailable\n# for the duration of the given period.\n#\n# The default is\n#\n# proxy.shutdownwait = 0s\n\n#proxy.deregistergraceperiod configures the time to wait before\n#shutting down the proxies de-registering from the service registry.\n#\n#After a signal is caught Fabio will immediately de-register from the\n#service registry and wait for `proxy.deregistergraceperiod` letting\n#in-flight requests finish after which it will continue with shutting\n#down the proxy.\n#\n#The default is\n#\n#proxy.deregistergraceperiod = 0s\n\n# proxy.responseheadertimeout configures the response header timeout.\n#\n# This configures the ResponseHeaderTimeout of the http.Transport.\n#\n# The default is\n#\n# proxy.responseheadertimeout     = 0s\n\n\n# proxy.keepalivetimeout configures the keep-alive timeout.\n#\n# This configures the KeepAliveTimeout of the network dialer.\n#\n# The default is\n#\n# proxy.keepalivetimeout     = 0s\n\n\n# proxy.idleconntimeout configures the idle connection timeout, when\n# to close (keep-alive) connections\n#\n# The default is\n#\n# proxy.idleconntimeout = 15s\n\n\n# proxy.dialtimeout configures the connection timeout for\n# outgoing connections.\n#\n# This configures the DialTimeout of the network dialer.\n#\n# The default is\n#\n# proxy.dialtimeout = 30s\n\n\n# proxy.flushinterval configures periodic flushing of the\n# response buffer for SSE (server-sent events) connections.\n# They are detected when the 'Accept' header is\n# 'text/event-stream'.\n#\n# The default is\n#\n# proxy.flushinterval = 1s\n\n\n# proxy.globalflushinterval configures periodic flushing of the\n# response buffer for non-SSE connections. By default it is not enabled.\n#\n# The default is\n#\n# proxy.globalflushinterval = 0\n\n\n# proxy.maxconn configures the maximum number of cached\n# incoming and outgoing connections.\n#\n# This configures the MaxIdleConnsPerHost of the http.Transport.\n#\n# The default is\n#\n# proxy.maxconn = 10000\n\n\n# proxy.header.clientip configures the header for the request ip.\n#\n# The remoteIP is taken from http.Request.RemoteAddr.\n#\n# The default is\n#\n# proxy.header.clientip =\n\n\n# proxy.header.tls configures the header to set for TLS connections.\n#\n# When set to a non-empty value the proxy will set this header on every\n# TLS request to the value of ${proxy.header.tls.value}\n#\n# The default is\n#\n# proxy.header.tls =\n# proxy.header.tls.value =\n\n\n# proxy.header.requestid configures the header for the adding a unique request id.\n# When set non-empty value the proxy will set this header on every request to the\n# unique UUID value.\n#\n# The default is\n#\n# proxy.header.requestid =\n\n\n# proxy.header.sts.maxage enables and configures the max-age of HSTS for TLS requests.\n# When set greater than zero this enables the Strict-Transport-Security header\n# and sets the max-age value in the header.\n#\n# The default is\n#\n# proxy.header.sts.maxage = 0\n\n\n# proxy.header.sts.subdomains instructs HSTS to include subdomains.\n# When set to true, the 'includeSubDomains' option will be added to\n# the Strict-Transport-Security header.\n#\n# The default is\n#\n# proxy.header.sts.subdomains = false\n\n\n# proxy.header.sts.preload instructs HSTS to include the preload directive.\n# When set to true, the 'preload' option will be added to the\n# Strict-Transport-Security header.\n#\n# Sending the preload directive from your site can have PERMANENT CONSEQUENCES\n# and prevent users from accessing your site and any of its subdomains if you\n# find you need to switch back to HTTP. Please read the details at\n# https://hstspreload.org/#removal before sending the header with \"preload\".\n#\n# The default is\n#\n# proxy.header.sts.preload = false\n\n\n# proxy.gzip.contenttype configures which responses should be compressed.\n#\n# By default, responses sent to the client are not compressed even if the\n# client accepts compressed responses by setting the 'Accept-Encoding: gzip'\n# header. By setting this value responses are compressed if the Content-Type\n# header of the response matches and the response is not already compressed.\n# The list of compressable content types is defined as a regular expression.\n# The regular expression must follow the rules outlined in golang.org/pkg/regexp.\n#\n# A typical example is\n#\n# proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\\\\+(json|xml))(;.*)?$\n#\n# The default is\n#\n# proxy.gzip.contenttype =\n\n\n# proxy.auth configures one or more auth schemes.\n#\n# Each auth scheme is configured with a list of\n# key/value options. Each source must have a unique\n# name which can then be referred to in a routing\n# rule.\n#\n#   name=<name>;type=<type>;opt=arg;opt[=arg];...\n#\n# The following types of auth schemes are available:\n#\n# Basic\n#\n# The basic auth scheme leverages http basic authentication using\n# one htpasswd file which is loaded at startup and by default is cached until\n# the service exits. However, it's possible to refresh htpasswd file\n# periodically by setting the refresh interval with 'refresh' option.\n#\n# The 'file' option contains the path to the htpasswd file. The 'realm'\n# option contains realm name (optional, default is the scheme name).\n# The 'refresh' option can set the htpasswd file refresh interval. Minimal\n# refresh interval is 1s to void busy loop.\n# By default refresh is disabled i.e. set to zero.\n#\n#   name=<name>;type=basic;file=p/creds.htpasswd;realm=foo\n#\n# Examples\n#\n#   # single basic auth scheme\n#\n#   name=mybasicauth;type=basic;file=p/creds.htpasswd;\n#\n#   # single basic auth scheme with refresh interval set to 30 seconds\n#\n#   name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s\n#\n#   # basic auth with multiple schemes\n#\n#   proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd\n#                name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm\n#\n#\n# proxy.grpcmaxrxmsgsize configures the grpc max receive message size in bytes.\n# The default is\n# proxy.grpcmaxrxmsgsize = 4194304\n#\n# proxy.grpcmaxtxmsgsize configures the grpc max transmit messsage size in bytes\n# The default is\n# proxy.grpcmaxtxmsgsize = 4194304\n#\n#\n# proxy.grpcshutdowntimeout configures the amount of time fabio will wait to attempt\n# to close the connection while waiting for grpc traffic to finish to a backend that's been\n# deregistered.  Default value is\n# proxy.grpcshutdowntimeout = 2s\n# setting to 0s disables the wait.\n\n# log.access.format configures the format of the access log.\n#\n# If the value is either 'common' or 'combined' then the logs are written in\n# the Common Log Format or the Combined Log Format as defined below:\n#\n# 'common':   $remote_host - - [$time_common] \"$request\" $response_status $response_body_size\n# 'combined': $remote_host - - [$time_common] \"$request\" $response_status $response_body_size \"$header.Referer\" \"$header.User-Agent\"\n#\n# Otherwise, the value is interpreted as a custom log format which is defined\n# with the following parameters. Providing an empty format when logging is\n# enabled is an error. To disable access logging leave the log.access.target\n# value empty.\n#\n#   $header.<name>           - request http header (name: [a-zA-Z0-9-]+)\n#   $remote_addr             - host:port of remote client\n#   $remote_host             - host of remote client\n#   $remote_port             - port of remote client\n#   $request                 - request <method> <uri> <proto>\n#   $request_args            - request query parameters\n#   $request_host            - request host header (aka server name)\n#   $request_method          - request method\n#   $request_scheme          - request scheme\n#   $request_uri             - request URI\n#   $request_url             - request URL\n#   $request_proto           - request protocol\n#   $response_body_size      - response body size in bytes\n#   $response_status         - response status code\n#   $response_time_ms        - response time in S.sss format\n#   $response_time_us        - response time in S.ssssss format\n#   $response_time_ns        - response time in S.sssssssss format\n#   $time_rfc3339            - log timestamp in YYYY-MM-DDTHH:MM:SSZ format\n#   $time_rfc3339_ms         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format\n#   $time_rfc3339_us         - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format\n#   $time_rfc3339_ns         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format\n#   $time_unix_ms            - log timestamp in unix epoch ms\n#   $time_unix_us            - log timestamp in unix epoch us\n#   $time_unix_ns            - log timestamp in unix epoch ns\n#   $time_common             - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ\n#   $upstream_addr           - host:port of upstream server\n#   $upstream_host           - host of upstream server\n#   $upstream_port           - port of upstream server\n#   $upstream_request_scheme - upstream request scheme\n#   $upstream_request_uri    - upstream request URI\n#   $upstream_request_url    - upstream request URL\n#   $upstream_service        - name of the upstream service\n#\n# The default is\n#\n# log.access.format = common\n\n\n# log.access.target configures where the access log is written to.\n#\n# Options are 'stdout'. If the value is empty no access log is written.\n#\n# The default is\n#\n# log.access.target =\n\n\n# log.level configures the log level.\n#\n# Valid levels are TRACE, DEBUG, INFO, WARN, ERROR and FATAL.\n#\n# The default is\n#\n# log.level = INFO\n\n\n# log.routes.format configures the log output format of routing table updates.\n#\n# Changes to the routing table are written to the standard log. This option\n# configures the output format:\n#\n# detail:   detailed routing table as ascii tree\n# delta:    additions and deletions in config language\n# all:      complete routing table in config language\n#\n# The default is\n#\n# log.routes.format = delta\n\n\n# registry.backend configures which backend is used.\n# Supported backends are: consul, static, file, custom\n# if custom is used fabio makes an api call to a remote system\n# expecting the below json response\n#   [\n#    {\n#       \"cmd\": \"string\",\n#       \"service\": \"string\",\n#       \"src\": \"string\",\n#       \"dst\": \"string\",\n#       \"weight\": float,\n#       \"tags\": [\"string\"],\n#       \"opts\": {\"string\":\"string\"}\n#     }\n#   ]\n# Short description of the fields required for a custom backend\n#\n# - cmd - the command to add, remove or change weight of a route. For example `route add` to add a new route mapping.\n# - service - the name that the service will show up in the UI.\n# - src - usually the prefix that will be used in the routing table.\n# - dst - the endpoint that will be used as the destination of the routing table.\n# - weight - defines the weight of this path to perform routing. For example route A 90% and route B 10% for canary deployments.\n# - tags - a list of tags, provide a way to filter routes, making it easier to do operations like bulk deletes `route del tags \"dev\"`.\n# - opts - a KV map of the config language list of options. for example `proto` or `prefix`\n#\n# The default is\n#\n# registry.backend = consul\n\n\n# registry.timeout configures how long fabio tries to connect to the registry\n# backend during startup.\n#\n# The default is\n#\n# registry.timeout = 10s\n\n\n# registry.retry configures the interval with which fabio tries to\n# connect to the registry during startup.\n#\n# The default is\n#\n# registry.retry = 500ms\n\n\n# registry.static.routes configures a static routing table.\n#\n# Example:\n#\n#     registry.static.routes = \\\n#       route add svc / http://1.2.3.4:5000/\n#\n# The default is\n#\n# registry.static.routes =\n\n\n# registry.static.noroutehtmlpath configures the KV path for the HTML of the\n# noroutes page.\n#\n# The default is\n#\n# registry.static.noroutehtmlpath =\n\n\n# registry.file.path configures a file based routing table.\n# The value configures the path to the file with the routing table.\n#\n# The default is\n#\n# registry.file.path =\n\n\n# registry.file.noroutehtmlpath configures the KV path for the HTML of the\n# noroutes page.\n#\n# The default is\n#\n# registry.file.noroutehtmlpath =\n\n\n# registry.consul.addr configures the address of the consul agent to connect to.\n#\n# The default is\n#\n# registry.consul.addr = localhost:8500\n\n\n# registry.consul.token configures the acl token for consul.\n#\n# The default is\n#\n# registry.consul.token =\n\n\n# registry.consul.tls.keyfile the path to the TLS certificate private key used for Consul communication.\n#\n# This is the full path to the TLS private key while using TLS transport to\n# communicate with Consul\n#\n# The default is\n#\n# registry.consul.tls.keyfile =\n\n# registry.consul.tls.certfile the path to the TLS certificate used for Consul communication.\n#\n# This is the full path to the TLS certificate while using TLS transport to\n# communicate with Consul\n#\n# The default is\n#\n# registry.consul.tls.certfile =\n\n\n# registry.consul.tls.cafile the path to the ca certificate used for Consul communication.\n#\n# This is the full path to the CA certificate while using TLS transport to\n# communicate with Consul\n#\n# The default is\n#\n# registry.consul.tls.cafile =\n\n\n# registry.consul.tls.capath the path to the folder containing CA certificates.\n#\n# This is the full path to the folder with CA certificates while using TLS transport to\n# communicate with Consul\n#\n# The default is\n#\n# registry.consul.tls.capath =\n\n\n# registry.consul.tls.insecureskipverify enable SSL verification with Consul.\n#\n# registry.consul.tls.insecureskipverify enables or disables SSL verification while using TLS transport to\n# communicate with Consul\n#\n# The default is\n#\n# registry.consul.tls.insecureskipverify = false\n\n\n# registry.consul.kvpath configures the KV path for manual routes.\n#\n# The consul KV path is watched for changes which get appended to\n# the routing table. This allows for manual overrides and weighted\n# round-robin routes. The key itself (e.g. fabio/config) and all\n# subkeys (e.g. fabio/config/foo and fabio/config/bar) are combined\n# in alphabetical order.\n#\n# The default is\n#\n# registry.consul.kvpath = /fabio/config\n\n\n# registry.consul.noroutehtmlpath configures the KV path for the HTML of the\n# noroutes page.\n#\n# The consul KV path is watched for changes.\n#\n# The default is\n#\n# registry.consul.noroutehtmlpath = /fabio/noroute.html\n\n# registry.consul.service.status configures the valid service status\n# values for services included in the routing table.\n#\n# The values are a comma separated list of\n# \"passing\", \"warning\", \"critical\" and \"unknown\"\n#\n# The default is\n#\n# registry.consul.service.status = passing\n\n\n# registry.consul.tagprefix configures the prefix for tags which define routes.\n#\n# Services which define routes publish one or more tags with host/path\n# routes which they serve. These tags must have this prefix to be\n# recognized as routes.\n#\n# The default is\n#\n# registry.consul.tagprefix = urlprefix-\n\n\n# registry.consul.register.enabled configures whether fabio registers itself in consul.\n#\n# Fabio will register itself in consul only if this value is set to \"true\" which\n# is the default. To disable registration set it to any other value, e.g. \"false\"\n#\n# The default is\n#\n# registry.consul.register.enabled = true\n\n\n# registry.consul.namespace configures the consul namespace in which fabio will register itself.\n#\n# Namespaces are a feature only available in the enterprise version of consul. In the open-source\n# version or with an empty namespace option fabio will be registered in the default namespace. Only\n# services running in the same consul namespace will be picked up by fabio.\n#\n# The default is\n#\n# registry.consul.namespace = \n\n\n# registry.consul.register.addr configures the address for the service registration.\n#\n# Fabio registers itself in consul with this host:port address.\n# It must point to the UI/API endpoint configured by ui.addr and defaults to its\n# value.\n#\n# The default is\n#\n# registry.consul.register.addr = :9998\n\n\n# registry.consul.register.name configures the name for the service registration.\n#\n# Fabio registers itself in consul under this service name.\n#\n# The default is\n#\n# registry.consul.register.name = fabio\n\n\n# registry.consul.register.tags configures the tags for the service registration.\n#\n# Fabio registers itself with these tags. You can provide a comma separated list of tags.\n#\n# The default is\n#\n# registry.consul.register.tags =\n\n\n# registry.consul.register.checkInterval configures the interval for the health check.\n#\n# Fabio registers an http health check on http(s)://${ui.addr}/health\n# and this value tells consul how often to check it.\n#\n# The default is\n#\n# registry.consul.register.checkInterval = 1s\n\n\n# registry.consul.register.checkTimeout configures the timeout for the health check.\n#\n# Fabio registers an http health check on http(s)://${ui.addr}/health\n# and this value tells consul how long to wait for a response.\n#\n# The default is\n#\n# registry.consul.register.checkTimeout = 3s\n\n\n# registry.consul.register.checkTLSSkipVerify configures TLS verification for the health check.\n#\n# Fabio registers an http health check on http(s)://${ui.addr}/health\n# and this value tells consul to skip TLS certificate validation for\n# https checks.\n#\n# The default is\n#\n# registry.consul.register.checkTLSSkipVerify = false\n\n\n# registry.consul.register.checkDeregisterCriticalServiceAfter configures\n# automatic deregistration of a service after the health check is critical for\n# this length of time.\n#\n# Fabio registers an http health check on http(s)://${ui.addr}/health\n# and this value tells consul to deregister the associated service if the check\n# is critical for the specified duration.\n#\n# The default is\n#\n# registry.consul.register.checkDeregisterCriticalServiceAfter = 90m\n\n\n# registry.consul.checksRequired configures how many health checks\n# must pass in order for fabio to consider a service available.\n#\n# Possible values are:\n#  one: at least one health check must pass\n#  all: all health checks must pass\n#\n# The default is\n#\n# registry.consul.checksRequired = one\n\n\n# registry.consul.serviceMonitors configures the concurrency for\n# route updates. Fabio will make up to the configured number of\n# concurrent calls to Consul to fetch status data for route\n# updates.\n#\n# The default is\n#\n# registry.consul.serviceMonitors = 1\n\n\n# registry.consul.pollInterval configures the poll interval\n# for route updates. If Poll interval is set to 0 the updates will\n# be disabled and fall back to blocking queries.  Other values can\n# be any time definition. e.g. 1s, 100ms\n#\n# The default is\n# registry.consul.pollInterval = 0\n\n\n# registry.custom.host configures the host:port for fabio to make the API call\n#\n# The default is\n#\n# registry.custom.host =\n\n\n# registry.custom.scheme configures the scheme use to make the API call\n# must be one of http, https\n#\n# The default is\n#\n# registry.custom.scheme = https\n\n\n# registry.custom.checkTLSSkipVerify disables the TLS validation for the API call\n#\n# The default is\n#\n# registry.custom.checkTLSSkipVerify = false\n\n\n# registry.custom.timeout controls the timeout for the API call\n#\n# The default is\n#\n# registry.custom.timeout = 5s\n\n\n# registry.custom.pollinterval is the length of time between API calls\n#\n# The default is\n#\n#registry.custom.pollinterval = 10s\n\n\n# registry.custom.path is the path used in the custom back end API Call\n#\n# The path does not need to contain the initial '/'\n#\n# Example:\n#\n#     registry.custom.path = api/v1/\n#\n# The default is\n#\n# registry.custom.path =\n\n\n# registry.custom.queryparams is the query parameters used in the custom back\n# end API Call\n#\n# Multiple query parameters should be separated with an &\n#\n# Example:\n#\n#     registry.custom.queryparams = foo=bar&bar=foo\n#\n# The default is\n#\n# registry.custom.queryparams =\n\n\n# glob.matching.disabled disables glob matching on route lookups\n# If glob matching is enabled there is a performance decrease\n# for every route lookup.  At a large number of services (> 500) this\n# can have a significant impact on performance. If glob matching is disabled\n# Fabio performs a static string compare for route lookups.\n#\n# The default is\n#\n# glob.matching.disabled = false\n\n# glob.cache.size sets the globCache size used for matching on route lookups.\n#\n# The default is\n#\n# glob.cache.size = 1000\n\n\n# metrics.target configures the backend the metrics values are\n# sent to.\n#\n# Possible values are:\n#  <empty>:    do not report metrics\n#  stdout:     report metrics to stdout\n#  graphite:   report metrics to Graphite on ${metrics.graphite.addr}\n#  statsd_raw: report metrics to StatsD on ${metrics.statsd.addr}\n#  dogstatsd:  report metrics to DogstatsD on ${metrics.dogstatsd.addr}\n#  circonus:   report metrics to Circonus (http://circonus.com/)\n#  prometheus: report metrics on a prometheus listener.  To combined with prometheus proxy.addr config\n#\n# The default is\n#\n# metrics.target =\n# note - multiple metrics targets can be defined separated by comma\n\n# metrics.prefix configures the template for the prefix of all reported metrics.\n#\n# Each metric has a unique name which is hard-coded to\n#\n#    prefix.service.host.path.target-addr\n#\n# The value is expanded by the text/template package and provides\n# the following variables:\n#\n#  - Hostname:  the Hostname of the server\n#  - Exec:      the executable name of application\n#\n# The following additional functions are defined:\n#\n#  - clean:     lowercase value and replace '.' and ':' with '_'\n#\n# Template may include regular string parts to customize final prefix\n#\n# Example:\n#\n#  Server hostname: test-001.something.com\n#  Binary executable name: fabio\n#\n#  The template variables are:\n#\n#  .Hostname =  test-001.something.com\n#  .Exec = fabio\n#\n# which results to the following prefix string when using the\n# default template:\n#\n#  test-001_something_com.fabio\n#\n# The default is\n#\n# metrics.prefix = {{clean .Hostname}}.{{clean .Exec}}\n\n\n# metrics.names configures the template for the route metric names\n# on backends that don't support tags.  This is used in circonus,\n# graphite and statsd_raw.  dogstatsd and prometheus ignore this.\n# The value is expanded by the text/template package and provides\n# the following variables:\n#\n#  - Service:   the service name\n#  - Host:      the host part of the URL prefix\n#  - Path:      the path part of the URL prefix\n#  - TargetURL: the URL of the target\n#\n# The following additional functions are defined:\n#\n#  - clean:     lowercase value and replace '.' and ':' with '_'\n#\n# Given a route rule of\n#\n#  route add testservice www.example.com/ http://10.1.2.3:12345/\n#\n# the template variables are:\n#\n#  .Service = testservice\n#  .Host = www.example.com\n#  .Path  = /\n#  .TargetURL.Host = 10.1.2.3:12345\n#\n# which results to the following metric name when using the\n# default template:\n#\n#  testservice.www_example_com./.10_1_2_3_12345\n#\n# The default is\n#\n# metrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}\n\n\n# metrics.interval configures the interval in which metrics are\n# reported.  This has no effect on prometheus.\n#\n# The default is\n#\n# metrics.interval = 30s\n\n\n# metrics.timeout configures how long fabio tries to connect to the metrics\n# backend during startup.\n#\n# The default is\n#\n# metrics.timeout = 10s\n\n\n# metrics.retry configures the interval with which fabio tries to\n# connect to the metrics backend during startup.\n#\n# The default is\n#\n# metrics.retry = 500ms\n\n\n# metrics.graphite.addr configures the host:port of the Graphite\n# server. This is required when ${metrics.target} is set to \"graphite\".\n#\n# The default is\n#\n# metrics.graphite.addr =\n\n\n# metrics.statsd.addr configures the host:port of the StatsD\n# server. This is required when ${metrics.target} is set to \"statsd_raw\".\n#\n# The default is\n#\n# metrics.statsd.addr =\n\n# metrics.dogstatsd.addr configures the host:port of the DogStatsD\n# server. This is required when ${metrics.target} is set to \"dogstatsd\".\n#\n# The default is\n#\n# metrics.dogstatsd.addr =\n\n\n# metrics.circonus.apikey configures the API token key to use when\n# submitting metrics to Circonus. See: https://login.circonus.com/user/tokens\n# This is optional when ${metrics.target} is set to \"circonus\" but\n# ${metrics.circonus.submissionurl is specified}.\n#\n# The default is\n#\n# metrics.circonus.apikey =\n\n\n# metrics.circonus.submissionurl configures a specific check submission url\n# for a Check API object of a previously created HTTPTRAP check\n# This is optional when ${metrics.target} is set to \"circonus\" but\n# ${metrics.circonus.apikey is specified}.\n# #### Example\n#\n# `http://127.0.0.1:2609/write/fabio`\n#\n# The default is\n#\n# metrics.circonus.submissionurl =\n\n\n# metrics.circonus.apiapp configures the API token app to use when\n# submitting metrics to Circonus. See: https://login.circonus.com/user/tokens\n# This is optional when ${metrics.target} is set to \"circonus\".\n#\n# The default is\n#\n# metrics.circonus.apiapp = fabio\n\n\n# metrics.circonus.apiurl configures the API URL to use when\n# submitting metrics to Circonus. https://api.circonus.com/v2/\n# will be used if no specific URL is provided.\n# This is optional when ${metrics.target} is set to \"circonus\".\n#\n# The default is\n#\n# metrics.circonus.apiurl =\n\n\n# metrics.circonus.brokerid configures a specific broker to use when\n# creating a check for submitting metrics to Circonus.\n# This is optional when ${metrics.target} is set to \"circonus\".\n# Optional for public brokers, required for Inside brokers.\n# Only applicable if a check is being created.\n#\n# The default is\n#\n# metrics.circonus.brokerid =\n\n\n# metrics.circonus.checkid configures a specific check to use when\n# submitting metrics to Circonus.\n# This is optional when ${metrics.target} is set to \"circonus\".\n# An attempt will be made to search for a previously created check,\n# if no applicable check is found, one will be created.\n#\n# The default is\n#\n# metrics.circonus.checkid =\n\n# metrics.prometheus.subsystem configures the system name when reporting\n# metrics.  This is basically appended to the prefix for metric names.\n#\n# The default is\n#\n# metrics.prometheus.subsystem =\n\n\n# metrics.prometheus.path configures the path to serve up metrics on any configured\n# proxy.addr's where proto=prometheus.\n#\n# The default is\n#\n# metrics.prometheus.path = /metrics\n\n# metrics.prometheus.buckets configures the time buckets for use with histograms, measured in seconds.\n# for instance, .005 is equivalent to 5ms.  there is an implied \"infinity\" bucket tacked on at the end.\n#\n# The default is\n#\n# metrics.prometheus.buckets = .005,.01,.025,.05,.1,.25,.5,1,2.5,5,10\n\n# runtime.gogc configures GOGC (the GC target percentage).\n#\n# Setting runtime.gogc is equivalent to setting the GOGC\n# environment variable which also takes precedence over\n# the value from the config file.\n#\n# NOTE - the default for fabio up to 1.5.14 was 800.  This changed\n# to 100 in version 1.5.15\n#\n# The default is\n#\n# runtime.gogc = 100\n\n\n# runtime.gomaxprocs configures GOMAXPROCS.\n#\n# Setting runtime.gomaxprocs is equivalent to setting the GOMAXPROCS\n# environment variable which also takes precedence over\n# the value from the config file.\n#\n# If runtime.gomaxprocs < 0 then all CPU cores are used.\n#\n# The default is\n#\n# runtime.gomaxprocs = -1\n\n\n# ui.access configures the access mode for the UI.\n#\n#  ro:  read-only access\n#  rw:  read-write access\n#\n# The default is\n#\n# ui.access = rw\n\n\n# ui.addr configures the address the UI is listening on.\n# The listener uses the same syntax as proxy.addr but\n# supports only a single listener. To enable HTTPS\n# configure a certificate source. You should use\n# a different certificate source than the one you\n# use for the external connections, e.g. 'cs=ui'.\n#\n# The default is\n#\n# ui.addr = :9998\n\n\n# ui.color configures the background color of the UI.\n# Color names are from http://materializecss.com/color.html\n#\n# The default is\n#\n# ui.color = light-green\n\n\n# ui.title configures an optional title for the UI.\n#\n# The default is\n#\n# ui.title =\n\n\n# ui.routingtable.source.linkenabled optionally configure if the\n# routing table's column \"source\" should be a clickable link.\n#\n# The default is\n#\n# ui.routingtable.source.linkenabled = false\n\n\n# ui.routingtable.source.newtab configures if the source\n# link should open in a new tab.\n# This is only applicable if the 'linkenabled' is set to true.\n#\n# The default is\n#\n# ui.routingtable.source.newtab = true\n\n\n# ui.routingtable.source.scheme configures the scheme protocol\n# for the link of the source on the routing table.  This is\n# useful when the scheme is different than the current page\n# or to force the traffic to a certain protocol.\n# This is only applicable if the 'linkenabled' is set to true.\n#\n# The default is\n#\n# ui.routingtable.source.scheme = http\n\n\n# ui.routingtable.source.host configures an optional host or\n# base address for the link in the source column.\n# This is only used when the source is not a separate\n# server (does not begin with '/', e.g. 'dev.google.net'). If\n# source is subdirectory it will set the link for the source to\n# this host.  If this is not set, and the source link is\n# enabled, the link will default to current host.\n# This is only applicable if the 'linkenabled' is set to true.\n#\n# The default is\n#\n# ui.routingtable.source.host =\n\n\n# ui.routingtable.source.port configures an optional port\n# for the routing table source column link.  This\n# is used in conjunction with the host and scheme.  If the\n# source is not a separate server (does not begin with '/',\n# e.g. 'dev.google.net'), and the host is set, or default to\n# the current scheme protocol port (80 for http or 443 for https).\n# This is only applicable if the 'linkenabled' is set to true.\n#\n# The default is\n#\n# ui.routingtable.source.port =\n\n\n# Open Trace Configuration Currently supports ZipKin Collector\n# tracing.TracingEnabled enables/disables  Open Tracing in Fabio.  Bool value true/false\n#\n# The default is\n#\n# tracing.TracingEnabled = false\n\n\n# tracing.CollectorType sets what type of collector is used.\n# Currently only two types are supported http and kafka\n#\n# http: sets collector type to http tracing.ConnectString must also be set\n# kafka: sets collector type to emit via kafka.  tracing.Topic must also be set\n#\n# The default is\n#\n# tracing.CollectorType = http\n\n\n# tracing.ConnectString sets the connection string per connection type.\n# If tracing.CollectorType = http tracing.ConnectString should be\n# http://URL:PORT where URL is the URL of your collector and PORT is the TCP Port\n# it is listening on\n#\n# If tracing.CollectorType = kafka tracing.ConnectString should be\n# HOSTNAME:PORT of your kafka broker\n# tracing.Topic must also be set\n#\n# The default is\n#\n# tracing.ConnectString = http://localhost:9411/api/v1/spans\n\n\n# tracing.ServiceName sets the service name used in reporting span information\n#\n# The default is\n#\n# tracing.ServiceName = Fabiolb\n\n\n# tracing.SpanName configures the template used in reporting span information\n#\n# The value is expanded by the text/template package and provides\n# the following variables:\n#\n#  - Proto:       the protocol version\n#  - Method:      the HTTP method\n#  - Host:        the host part of the URL\n#  - Scheme:      the scheme of the requested URL\n#  - Path:        the path of the requested URL\n#  - RawQuery:    the encoded query values of the requested URL\n#\n# SpanName defaults to the value of tracing.ServiceName but can be\n# overridden with this property.\n#\n# Example: tracing.SpanName = {{.Proto}} {{.Method}} {{.Path}}\n#\n# The default is\n#\n# tracing.SpanName =\n\n\n# tracing.Topic sets the Topic String used if tracing.CollectorType is kafka and\n# tracing.ConnectSting is set to a kafka broker\n#\n# The default is\n#\n# tracing.Topic = Fabiolb-Kafka-Topic\n\n\n# tracing.SamplerRate is the rate at which opentrace span data will be collected and sent\n# If SamplerRate is <= 0 Never sample\n# If SamplerRate is >= 1.0 always sample\n# Values between 0 and 1 will be the percentage in decimal form\n# Example a value of .50 will be 50% sample rate\n#\n# The default is\n# tracing.SamplerRate = -1\n\n\n# tracing.SpanHost sets host information.\n# This is used to specify additional information when sending spans to a collector\n#\n# The default is\n# tracing.SpanHost = localhost:9998\n\n# BGP Anycast configuration\n# Experimental.  Leopards will eat your face.\n\n# bgp.enabled enables the embedded gobgpd daemon.\n# The default is\n\n# bgp.enabled = false\n\n# bgp.asn sets the asn ID of our router\n# The default is:\n# bgp.asn = 65000\n\n# bgp.anycastaddresses sets the anycast addresses we will advertise, separated by comma.  Technically this\n# will advertise any route prefix.  These should already be configured on the host probably hung off loopback.\n# for example, 192.168.5.3/32.  The default value is:\n\n# bgp.anycastaddresses =\n#\n# If bgp is enabled, this must be defined.\n\n# bgp.routerid is the router id (ip address) of this router.  This is required if bgp is enabled.\n# the default value is:\n\n# bgp.routerid =\n\n#\n# bgp.listenport sets the listen ports for bgp communication from other routers.\n# default vaule is :\n\n# bgp.listenport = 179\n\n# bgp.listenaddresses sets the listen addresses for bgp, separated by comma.  The default is\n\n# bgp.listenaddresses = 0.0.0.0\n\n# which listens on all interfaces.\n\n# bgp.nexthop sets the next hop address.  If not set, it uses the bgp.routerid instead.\n# default value:\n\n# bgp.nexthop =\n\n# bgp.peers sets the bgp peers we will advertise routes to.  This is required if bgp is enabled.\n# bgp.peers is specified as a comma separated list of neighboraddress and asn pairs, i.e.\n# bgp.peers = address=1.2.3.4;asn=65001,address=5.6.7.8;asn=65002\n# valid parameters for peers are:\n#    address - required\n#    port - optional, defaults to 179\n#    asn - required\n#    multihop - optional, defaults to false\n#    multihoplength - optional, defaults to 2\n#    password - optional\n\n# default value\n# bgp.peers =\n\n# bgp.enablegrpc enables the gobgp grpc interface.  To be used with the gobgp command line client.\n# default value is:\n\n# bgp.enablegrpc=false\n\n# bgp.grpclistenaddress is the listen interface and port if bgp.enablegrpc is set to true.  defaults to:\n\n# bgp.grpclistenaddress = 127.0.0.1:50051\n\n# bgp.grpctls is whether to enable TLS on the bgp grpc interface.  default value is:\n\n# bgp.grpctls = false\n\n# bgp.certfile is the file path of the certificate, and is required if bgp.grpctls is set to true.  Default value is:\n\n# bgp.certfile =\n\n# bgp.keyfile is the file path of the key file, and is required if bgp.grpctls is set to true.  Default value is:\n\n# bgp.keyfile =\n\n# bgp.nexthop explicitly sets the value of the nexthop for all routes we publish.  If not set, this uses the\n# bgp.routerid value, which is what makes sense in most cases.  Default value is:\n\n# bgp.nexthop =\n\n# bgp.gobgpdcfgfile is the optional file path to a gobgpd config file.  This overrides the global config\n# items above, such as bgp.routerid, bgp.asn etc.  This also skips\n# # automatically adding gobgpd policies that prevent us from accepting prefixes from neighbors.  only\n# use this if you know what you're doing, this is to allow for more flexibility than we expose directly\n# with fabio.\n\n# default value is:\n# bgp.gobgpdcfgfile =\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/fabiolb/fabio\n\ngo 1.25.5\n\nrequire (\n\tgithub.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f\n\tgithub.com/circonus-labs/circonus-gometrics/v3 v3.4.7\n\tgithub.com/go-kit/kit v0.13.0\n\tgithub.com/go-kit/log v0.2.1\n\tgithub.com/gobwas/glob v0.2.3\n\tgithub.com/hashicorp/consul/api v1.33.0\n\tgithub.com/hashicorp/go-sockaddr v1.0.7\n\tgithub.com/hashicorp/vault/api v1.22.0\n\tgithub.com/hashicorp/vault/sdk v0.20.0\n\tgithub.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252\n\tgithub.com/magiconair/properties v1.8.10\n\tgithub.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9\n\tgithub.com/osrg/gobgp/v3 v3.37.0\n\tgithub.com/pascaldekloe/goe v0.1.1\n\tgithub.com/pkg/profile v1.7.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/rogpeppe/fastuuid v1.2.0\n\tgithub.com/sergi/go-diff v1.4.0\n\tgithub.com/tg123/go-htpasswd v1.2.4\n\tgolang.org/x/net v0.48.0\n\tgolang.org/x/sync v0.19.0\n\tgoogle.golang.org/grpc v1.77.0\n\tgoogle.golang.org/protobuf v1.36.10\n)\n\nrequire (\n\tgithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect\n\tgithub.com/VividCortex/gohistogram v1.0.0 // indirect\n\tgithub.com/armon/go-metrics v0.4.1 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/circonus-labs/go-apiclient v0.7.24 // indirect\n\tgithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect\n\tgithub.com/eapache/channels v1.1.0 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/fgprof v0.9.5 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logfmt/logfmt v0.6.1 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/go-immutable-radix v1.3.1 // indirect\n\tgithub.com/hashicorp/go-metrics v0.5.4 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8 // indirect\n\tgithub.com/hashicorp/go-rootcerts v1.0.2 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect\n\tgithub.com/hashicorp/golang-lru v1.0.2 // indirect\n\tgithub.com/hashicorp/hcl v1.0.1-vault-7 // indirect\n\tgithub.com/hashicorp/serf v0.10.2 // indirect\n\tgithub.com/k-sone/critbitgo v1.4.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/openhistogram/circonusllhist v0.4.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.4 // indirect\n\tgithub.com/prometheus/procfs v0.19.2 // indirect\n\tgithub.com/ryanuber/go-glob v1.0.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c // indirect\n\tgithub.com/vishvananda/netlink v1.3.1 // indirect\n\tgithub.com/vishvananda/netns v0.0.5 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.46.0 // indirect\n\tgolang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect\n\tgolang.org/x/sys v0.39.0 // indirect\n\tgolang.org/x/text v0.32.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=\ngithub.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=\ngithub.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=\ngithub.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=\ngithub.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f h1:SaJ6yqg936TshyeFZqQE+N+9hYkIeL9AMr7S4voCl10=\ngithub.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=\ngithub.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=\ngithub.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonus-gometrics/v3 v3.4.7 h1:r7YBLIgiTT5Q4yNKSouP68M5mYT4djIQCng0dJdidiA=\ngithub.com/circonus-labs/circonus-gometrics/v3 v3.4.7/go.mod h1:57gznrTyBxQCsGYC/+3BN9POyrkEm4F3KsBTVQ5EQJk=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/circonus-labs/go-apiclient v0.7.15/go.mod h1:RFgkvdYEkimzgu3V2vVYlS1bitjOz1SF6uw109ieNeY=\ngithub.com/circonus-labs/go-apiclient v0.7.24 h1:ouJ/Dd/mlKOpG2ZRkuAvBBCn/YRQq4762MOnwIGdYQ8=\ngithub.com/circonus-labs/go-apiclient v0.7.24/go.mod h1:M284FyvP8iLy5SPxLxy5yrOxEjK8RSgRPKhcc6WFDA4=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=\ngithub.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=\ngithub.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=\ngithub.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=\ngithub.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=\ngithub.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=\ngithub.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=\ngithub.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=\ngithub.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=\ngithub.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16 h1:ptucaU8cwiAc+/jqDblz0kb1ECLqPTeX/qQym8OBYzY=\ngithub.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/consul/api v1.33.0 h1:MnFUzN1Bo6YDGi/EsRLbVNgA4pyCymmcswrE5j4OHBM=\ngithub.com/hashicorp/consul/api v1.33.0/go.mod h1:vLz2I/bqqCYiG0qRHGerComvbwSWKswc8rRFtnYBrIw=\ngithub.com/hashicorp/consul/sdk v0.17.0 h1:N/JigV6y1yEMfTIhXoW0DXUecM2grQnFuRpY7PcLHLI=\ngithub.com/hashicorp/consul/sdk v0.17.0/go.mod h1:8dgIhY6VlPUprRH7o7UenVuFEgq017qUn3k9wS5mCt4=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=\ngithub.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=\ngithub.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=\ngithub.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0=\ngithub.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=\ngithub.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=\ngithub.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=\ngithub.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=\ngithub.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=\ngithub.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=\ngithub.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=\ngithub.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI=\ngithub.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4=\ngithub.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=\ngithub.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=\ngithub.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=\ngithub.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=\ngithub.com/hashicorp/vault/sdk v0.20.0 h1:a4ulj2gICzw/qH0A4+6o36qAHxkUdcmgpMaSSjqE3dc=\ngithub.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk=\ngithub.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=\ngithub.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252 h1:jeqlfkFa5h+Ak/I33QpU4p01nFhw0G5IFm/Rsenne2Y=\ngithub.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252/go.mod h1:R6mExYS3O0XXjOZye3GtXfbuGF4hWQnF45CFWoj7O6g=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrbE=\ngithub.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=\ngithub.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 h1:62uLwA3l2JMH84liO4ZhnjTH5PjFyCYxbHLgXPaJMtI=\ngithub.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9/go.mod h1:MvMXoufZAtqExNexqi4cjrNYE9MefKddKylxjS+//n0=\ngithub.com/openhistogram/circonusllhist v0.3.0/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0=\ngithub.com/openhistogram/circonusllhist v0.4.1 h1:oolOK2dTxFPIu9epYukSgINQ5no58iAWZfQ5SZt23vk=\ngithub.com/openhistogram/circonusllhist v0.4.1/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/osrg/gobgp/v3 v3.37.0 h1:+ObuOdvj7G7nxrT0fKFta+EAupdWf/q1WzbXydr8IOY=\ngithub.com/osrg/gobgp/v3 v3.37.0/go.mod h1:kVHVFy1/fyZHJ8P32+ctvPeJogn9qKwa1YCeMRXXrP0=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.1 h1:Ah6WQ56rZONR3RW3qWa2NCZ6JAVvSpUcoLBaOmYFt9Q=\ngithub.com/pascaldekloe/goe v0.1.1/go.mod h1:KSyfaxQOh0HZPjDP1FL/kFtbqYqrALJTaMafFUIccqU=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=\ngithub.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=\ngithub.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=\ngithub.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=\ngithub.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=\ngithub.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=\ngithub.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=\ngithub.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=\ngithub.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=\ngithub.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=\ngolang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=\ngolang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=\ngolang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=\ngolang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=\ngoogle.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=\n"
  },
  {
    "path": "logger/level_writer.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"sync/atomic\"\n)\n\n// LevelWriter implements a simplistic levelled log writer which supports\n// TRACE, DEBUG, INFO, WARN, ERROR and FATAL. The log level can be changed at\n// runtime.\ntype LevelWriter struct {\n\tw         io.Writer\n\tlevel     atomic.Value // string\n\tprefixLen int\n}\n\n// NewLevelWriter creates a new leveled writer for the given output and a\n// default level. Prefix is the string that is expected before the opening\n// bracket and usually depends on the chosen log format. For the default log\n// format prefix should be set to \"2017/01/01 00:00:00 \" whereby only the\n// format and the spaces are relevant but not the date and time itself.\nfunc NewLevelWriter(w io.Writer, level, prefix string) *LevelWriter {\n\tlw := &LevelWriter{w: w, prefixLen: len(prefix)}\n\tif !lw.SetLevel(level) {\n\t\tpanic(fmt.Sprintf(\"invalid log level %s\", level))\n\t}\n\treturn lw\n}\n\nfunc (w *LevelWriter) Write(b []byte) (int, error) {\n\t// check if the log line starts with the prefix\n\tif len(b) < w.prefixLen+2 || b[w.prefixLen] != '[' {\n\t\treturn fmt.Fprint(w.w, \"invalid log msg: \", string(b))\n\t}\n\n\t// determine the level by looking at the character after the opening\n\t// bracket.\n\tlevel := rune(b[w.prefixLen+1]) // T, D, I, W, E, or F\n\n\t// w.level contains the characters of all the allowed levels so we can just\n\t// check whether the level character is in that set.\n\tif strings.ContainsRune(w.level.Load().(string), level) {\n\t\treturn w.w.Write(b)\n\t}\n\treturn 0, nil\n}\n\n// SetLevel sets the log level to the new value and returns true\n// if that was successful.\nfunc (w *LevelWriter) SetLevel(s string) bool {\n\t// levels contains the first character of the levels in descending order\n\tconst levels = \"TDIWEF\"\n\tswitch strings.ToUpper(s) {\n\tcase \"TRACE\":\n\t\tw.level.Store(levels[0:])\n\t\treturn true\n\tcase \"DEBUG\":\n\t\tw.level.Store(levels[1:])\n\t\treturn true\n\tcase \"INFO\":\n\t\tw.level.Store(levels[2:])\n\t\treturn true\n\tcase \"WARN\":\n\t\tw.level.Store(levels[3:])\n\t\treturn true\n\tcase \"ERROR\":\n\t\tw.level.Store(levels[4:])\n\t\treturn true\n\tcase \"FATAL\":\n\t\tw.level.Store(levels[5:])\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Level returns the current log level.\nfunc (w *LevelWriter) Level() string {\n\tl := w.level.Load().(string)\n\tswitch l[0] {\n\tcase 'T':\n\t\treturn \"TRACE\"\n\tcase 'D':\n\t\treturn \"DEBUG\"\n\tcase 'I':\n\t\treturn \"INFO\"\n\tcase 'W':\n\t\treturn \"WARN\"\n\tcase 'E':\n\t\treturn \"ERROR\"\n\tcase 'F':\n\t\treturn \"FATAL\"\n\tdefault:\n\t\treturn \"???\" + l + \"???\"\n\t}\n}\n"
  },
  {
    "path": "logger/level_writer_test.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestLevelWriter(t *testing.T) {\n\tinput := []string{\n\t\t\"2017/01/01 00:00:00 [TRACE] a\",\n\t\t\"2017/01/01 00:00:00 [DEBUG] a\",\n\t\t\"2017/01/01 00:00:00 [INFO] a\",\n\t\t\"2017/01/01 00:00:00 [WARN] a\",\n\t\t\"2017/01/01 00:00:00 [ERROR] a\",\n\t\t\"2017/01/01 00:00:00 [FATAL] a\",\n\t}\n\ttests := []struct {\n\t\tlevel string\n\t\tout   []string\n\t}{\n\t\t{\"TRACE\", input},\n\t\t{\"DEBUG\", input[1:]},\n\t\t{\"INFO\", input[2:]},\n\t\t{\"WARN\", input[3:]},\n\t\t{\"ERROR\", input[4:]},\n\t\t{\"FATAL\", input[5:]},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.level, func(t *testing.T) {\n\t\t\tvar b bytes.Buffer\n\t\t\tw := NewLevelWriter(&b, tt.level, \"2017/01/01 00:00:00 \")\n\t\t\tfor _, s := range input {\n\t\t\t\tif _, err := w.Write([]byte(s + \"\\n\")); err != nil {\n\t\t\t\t\tt.Fatal(\"w.Write:\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tout := strings.Split(strings.TrimRight(b.String(), \"\\n\"), \"\\n\")\n\t\t\tif got, want := out, tt.out; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"got %#v want %#v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "logger/logger.go",
    "content": "// Package logger implements a configurable access logger.\n//\n// The access log format is defined through a format string which expands to a\n// log line per request. The values are taken as is and no quoting or escaping\n// takes place. Text between two fields is printed verbatim. See the common\n// log file formats for an example.\n//\n//\t$header.<name>           - request http header (name: [a-zA-Z0-9-]+)\n//\t$remote_addr             - host:port of remote client\n//\t$remote_host             - host of remote client\n//\t$remote_port             - port of remote client\n//\t$request                 - request <method> <uri> <proto>\n//\t$request_args            - request query parameters\n//\t$request_host            - request host header (aka server name)\n//\t$request_method          - request method\n//\t$request_scheme          - request scheme\n//\t$request_uri             - request URI\n//\t$request_url             - request URL\n//\t$request_proto           - request protocol\n//\t$response_body_size      - response body size in bytes\n//\t$response_status         - response status code\n//\t$response_time_ms        - response time in S.sss format\n//\t$response_time_us        - response time in S.ssssss format\n//\t$response_time_ns        - response time in S.sssssssss format\n//\t$time_rfc3339            - log timestamp in YYYY-MM-DDTHH:MM:SSZ format\n//\t$time_rfc3339_ms         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format\n//\t$time_rfc3339_us         - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format\n//\t$time_rfc3339_ns         - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format\n//\t$time_unix_ms            - log timestamp in unix epoch ms\n//\t$time_unix_us            - log timestamp in unix epoch us\n//\t$time_unix_ns            - log timestamp in unix epoch ns\n//\t$time_common             - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ\n//\t$upstream_addr           - host:port of upstream server\n//\t$upstream_host           - host of upstream server\n//\t$upstream_port           - port of upstream server\n//\t$upstream_request_scheme - upstream request scheme\n//\t$upstream_request_uri    - upstream request URI\n//\t$upstream_request_url    - upstream request URL\npackage logger\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Common log file formats.\nconst (\n\tCommonFormat   = `$remote_host - - [$time_common] \"$request\" $response_status $response_body_size`\n\tCombinedFormat = `$remote_host - - [$time_common] \"$request\" $response_status $response_body_size \"$header.Referer\" \"$header.User-Agent\"`\n)\n\n// Event defines the elements of a loggable event.\ntype Event struct {\n\t// Start is the time when the action that triggered the event started.\n\tStart time.Time\n\n\t// End is the time when the action that triggered the event was completed.\n\tEnd time.Time\n\n\t// Request is the HTTP request that is connected to this event.\n\t// It should only be set for HTTP log events.\n\tRequest *http.Request\n\n\t// Response is the HTTP response which is connected to this event.\n\t// It should only be set for HTTP log events.\n\tResponse *http.Response\n\n\t// RequestURL is the URL of the incoming HTTP request.\n\t// It should only be set for HTTP log events.\n\tRequestURL *url.URL\n\n\t// UpstreamURL is the URL which was sent to the upstream server.\n\t// It should only be set for HTTP log events.\n\tUpstreamURL *url.URL\n\n\t// UpstreamAddr is the TCP address in the form of \"host:port\" of the\n\t// upstream server which handled the proxied request.\n\tUpstreamAddr string\n\n\t// UpstreamService is the name of the upstream service as\n\t// defined in the route.\n\tUpstreamService string\n}\n\n// Logger logs an event.\ntype Logger interface {\n\tLog(event *Event)\n}\n\n// New creates a new logger that writes log events in the given format to the\n// provided writer. If no writer was provided no log output is generated.\n// If the format is empty or invalid an error is returned.\nfunc New(w io.Writer, format string) (Logger, error) {\n\tif w == nil {\n\t\treturn &noopLogger{}, nil\n\t}\n\tp, err := parse(format, fields)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(p) == 0 {\n\t\treturn nil, errors.New(\"empty log format\")\n\t}\n\treturn &logger{p: p, w: w}, nil\n}\n\ntype noopLogger struct{}\n\nfunc (l *noopLogger) Log(*Event) {}\n\ntype logger struct {\n\tw io.Writer\n\tp pattern\n\n\tmu sync.Mutex\n}\n\n// bufSize defines the default size of the log buffers.\nconst bufSize = 1024\n\n// pool provides a reusable set of log buffers.\nvar pool = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn bytes.NewBuffer(make([]byte, 0, bufSize))\n\t},\n}\n\n// Log writes a log line for the request that was executed\n// between t1 and t2.\nfunc (l *logger) Log(e *Event) {\n\tb := pool.Get().(*bytes.Buffer)\n\tb.Reset()\n\tl.p.write(b, e)\n\tl.mu.Lock()\n\tl.w.Write(b.Bytes())\n\tl.mu.Unlock()\n\tpool.Put(b)\n}\n"
  },
  {
    "path": "logger/logger_test.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"text/template\"\n\t\"time\"\n)\n\nfunc TestParse(t *testing.T) {\n\tfields := map[string]field{\n\t\t\"$a\": func(b *bytes.Buffer, e *Event) {\n\t\t\tb.WriteString(\"aa\")\n\t\t},\n\t\t\"$b\": func(b *bytes.Buffer, e *Event) {\n\t\t\tb.WriteString(\"bb\")\n\t\t},\n\t}\n\treq := &http.Request{\n\t\tHeader: http.Header{\n\t\t\t\"User-Agent\":      {\"Mozilla Firefox\"},\n\t\t\t\"X-Forwarded-For\": {\"3.3.3.3\"},\n\t\t},\n\t}\n\ttests := []struct {\n\t\tformat string\n\t\tout    string\n\t}{\n\t\t{\"\", \"\"},\n\t\t{\"$a\", \"aa\\n\"},\n\t\t{\"$a $b\", \"aa bb\\n\"},\n\t\t{\"$a \\\"$header.User-Agent\\\"\", \"aa \\\"Mozilla Firefox\\\"\\n\"},\n\t}\n\n\tfor i, tt := range tests {\n\t\tp, err := parse(tt.format, fields)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%d: got %v want nil\", i, err)\n\t\t\tcontinue\n\t\t}\n\t\tvar b bytes.Buffer\n\t\tp.write(&b, &Event{Start: time.Time{}, End: time.Time{}, Request: req})\n\t\tif got, want := b.String(), tt.out; got != want {\n\t\t\tt.Errorf(\"%d: got %q want %q\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestLog(t *testing.T) {\n\trurl := mustParse(\"http://foo.com/?q=x\")\n\tuurl := mustParse(\"http://7.8.9.0:5678/foo?q=x\")\n\tstart := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)\n\te := &Event{\n\t\tStart: start,\n\t\tEnd:   start.Add(123456789 * time.Nanosecond),\n\t\tRequest: &http.Request{\n\t\t\tRequestURI: rurl.RequestURI(),\n\t\t\tHeader: http.Header{\n\t\t\t\t\"User-Agent\":      {\"Mozilla Firefox\"},\n\t\t\t\t\"Referer\":         {\"http://foo.com/\"},\n\t\t\t\t\"X-Forwarded-For\": {\"3.3.3.3\"},\n\t\t\t},\n\t\t\tRemoteAddr: \"2.2.2.2:666\",\n\t\t\tHost:       rurl.Host,\n\t\t\tURL:        rurl,\n\t\t\tMethod:     \"GET\",\n\t\t\tProto:      \"HTTP/1.1\",\n\t\t},\n\t\tResponse: &http.Response{\n\t\t\tStatusCode:    200,\n\t\t\tContentLength: 1234,\n\t\t\tHeader:        http.Header{\"foo\": []string{\"bar\"}},\n\t\t\tRequest: &http.Request{\n\t\t\t\tRemoteAddr: \"5.6.7.8:1234\",\n\t\t\t},\n\t\t},\n\t\tRequestURL:      rurl,\n\t\tUpstreamAddr:    uurl.Host,\n\t\tUpstreamService: \"svc-a\",\n\t\tUpstreamURL:     uurl,\n\t}\n\n\ttests := []struct {\n\t\tformat string\n\t\tout    string\n\t}{\n\t\t{\"$header.Referer\", \"http://foo.com/\\n\"},\n\t\t{\"$header.X-Forwarded-For\", \"3.3.3.3\\n\"},\n\t\t{\"$header.user-agent\", \"Mozilla Firefox\\n\"},\n\t\t{\"$remote_addr\", \"2.2.2.2:666\\n\"},\n\t\t{\"$remote_host\", \"2.2.2.2\\n\"},\n\t\t{\"$remote_port\", \"666\\n\"},\n\t\t{\"$request\", \"GET /?q=x HTTP/1.1\\n\"},\n\t\t{\"$request_args\", \"q=x\\n\"},\n\t\t{\"$request_host\", \"foo.com\\n\"}, // TODO(fs): is this correct?\n\t\t{\"$request_method\", \"GET\\n\"},\n\t\t{\"$request_proto\", \"HTTP/1.1\\n\"},\n\t\t{\"$request_scheme\", \"http\\n\"},\n\t\t{\"$request_uri\", \"/?q=x\\n\"},\n\t\t{\"$request_url\", \"http://foo.com/?q=x\\n\"},\n\t\t{\"$response_body_size\", \"1234\\n\"},\n\t\t{\"$response_status\", \"200\\n\"},\n\t\t{\"$response_time_ms\", \"0.123\\n\"},       // TODO(fs): is this correct?\n\t\t{\"$response_time_ns\", \"0.123456789\\n\"}, // TODO(fs): is this correct?\n\t\t{\"$response_time_us\", \"0.123456\\n\"},    // TODO(fs): is this correct?\n\t\t{\"$time_common\", \"01/Jan/2016:00:00:00 +0000\\n\"},\n\t\t{\"$time_rfc3339\", \"2016-01-01T00:00:00Z\\n\"},\n\t\t{\"$time_rfc3339_ms\", \"2016-01-01T00:00:00.123Z\\n\"},\n\t\t{\"$time_rfc3339_ns\", \"2016-01-01T00:00:00.123456789Z\\n\"},\n\t\t{\"$time_rfc3339_us\", \"2016-01-01T00:00:00.123456Z\\n\"},\n\t\t{\"$time_unix_ms\", \"1451606400123\\n\"},\n\t\t{\"$time_unix_ns\", \"1451606400123456789\\n\"},\n\t\t{\"$time_unix_us\", \"1451606400123456\\n\"},\n\t\t{\"$upstream_addr\", \"7.8.9.0:5678\\n\"},\n\t\t{\"$upstream_host\", \"7.8.9.0\\n\"},\n\t\t{\"$upstream_port\", \"5678\\n\"},\n\t\t{\"$upstream_request_scheme\", \"http\\n\"},\n\t\t{\"$upstream_request_uri\", \"/foo?q=x\\n\"},\n\t\t{\"$upstream_request_url\", \"http://7.8.9.0:5678/foo?q=x\\n\"},\n\t\t{\"$upstream_service\", \"svc-a\\n\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.format, func(t *testing.T) {\n\t\t\tb := new(bytes.Buffer)\n\n\t\t\tl, err := New(b, tt.format)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"got %v want nil\", err)\n\t\t\t}\n\n\t\t\tl.Log(e)\n\t\t\tif got, want := b.String(), tt.out; got != want {\n\t\t\t\tt.Errorf(\"got %q want %q\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAtoi(t *testing.T) {\n\ttests := []struct {\n\t\ti   int64\n\t\tpad int\n\t\ts   string\n\t}{\n\t\t{i: 0, pad: 0, s: \"0\"},\n\t\t{i: 1, pad: 0, s: \"1\"},\n\t\t{i: -1, pad: 0, s: \"-1\"},\n\t\t{i: 12345, pad: 0, s: \"12345\"},\n\t\t{i: -12345, pad: 0, s: \"-12345\"},\n\t\t{i: 9223372036854775807, pad: 0, s: \"9223372036854775807\"},\n\t\t{i: -9223372036854775807, pad: 0, s: \"-9223372036854775807\"},\n\n\t\t{i: 0, pad: 5, s: \"00000\"},\n\t\t{i: 1, pad: 5, s: \"00001\"},\n\t\t{i: -1, pad: 5, s: \"-00001\"},\n\t\t{i: 12345, pad: 5, s: \"12345\"},\n\t\t{i: -12345, pad: 5, s: \"-12345\"},\n\t\t{i: 9223372036854775807, pad: 5, s: \"9223372036854775807\"},\n\t\t{i: -9223372036854775807, pad: 5, s: \"-9223372036854775807\"},\n\t}\n\n\tfor i, tt := range tests {\n\t\tvar b bytes.Buffer\n\t\tatoi(&b, tt.i, tt.pad)\n\t\tif got, want := b.String(), tt.s; got != want {\n\t\t\tt.Errorf(\"%d: got %q want %q\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc BenchmarkLog(b *testing.B) {\n\tstart := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)\n\te := &Event{\n\t\tStart: start,\n\t\tEnd:   start.Add(100 * time.Millisecond),\n\t\tRequest: &http.Request{\n\t\t\tRequestURI: \"/?q=x\",\n\t\t\tHeader: http.Header{\n\t\t\t\t\"User-Agent\":      {\"Mozilla Firefox\"},\n\t\t\t\t\"Referer\":         {\"http://foo.com/\"},\n\t\t\t\t\"X-Forwarded-For\": {\"3.3.3.3\"},\n\t\t\t},\n\t\t\tRemoteAddr: \"2.2.2.2:666\",\n\t\t\tHost:       \"foo.com\",\n\t\t\tURL: &url.URL{\n\t\t\t\tPath:     \"/\",\n\t\t\t\tRawQuery: \"?q=x\",\n\t\t\t\tHost:     \"proxy host\",\n\t\t\t},\n\t\t\tMethod: \"GET\",\n\t\t\tProto:  \"HTTP/1.1\",\n\t\t},\n\t\tResponse: &http.Response{\n\t\t\tStatusCode:    200,\n\t\t\tContentLength: 1234,\n\t\t\tHeader:        http.Header{\"foo\": []string{\"bar\"}},\n\t\t\tRequest: &http.Request{\n\t\t\t\tRemoteAddr: \"5.6.7.8:1234\",\n\t\t\t},\n\t\t},\n\t\tUpstreamAddr: mustParse(\"http://7.8.9.0:5678/foo\").Host,\n\t}\n\n\t// benchmark the custom parser and text/template\n\t// to explain why there is a custom approach.\n\t// The custom parser is 8x faster and has zero allocs.\n\t//\n\t// BenchmarkLog/my_parser-8         \t 1000000\t      2326 ns/op\t       0 B/op\t       0 allocs/op\n\t// BenchmarkLog/go_text/template-8  \t  100000\t     19026 ns/op\t     848 B/op\t      76 allocs/op\n\tb.Run(\"custom parser\", func(b *testing.B) {\n\t\tvar keys []string\n\t\tfor k := range fields {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\tsort.Strings(keys)\n\t\tformat := strings.Join(keys, \" \")\n\n\t\tl, err := New(io.Discard, format)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor range b.N {\n\t\t\tl.Log(e)\n\t\t}\n\t})\n\tb.Run(\"text/template\", func(b *testing.B) {\n\t\t// simulate the text template approach by using\n\t\t// the same number of fields as for the other parser\n\t\t// but using the same value.\n\t\ttmpl := \"\"\n\t\tfor range fields {\n\t\t\ttmpl += \"{{.Req.RemoteAddr}}\"\n\t\t}\n\t\tt := template.Must(template.New(\"log\").Parse(tmpl))\n\n\t\tb.ResetTimer()\n\t\tfor range b.N {\n\t\t\tt.Execute(io.Discard, e)\n\t\t}\n\t})\n}\n\nfunc mustParse(s string) *url.URL {\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n"
  },
  {
    "path": "logger/pattern.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc init() {\n\tfor f := range fields {\n\t\tFields = append(Fields, f)\n\t}\n\tsort.Strings(Fields)\n}\n\n// Fields contains a list of all known static log fields in alphabetical order.\nvar Fields []string\n\n// pattern is a log output format.\ntype pattern []field\n\nfunc (p pattern) write(b *bytes.Buffer, e *Event) {\n\tfor _, fn := range p {\n\t\tfn(b, e)\n\t}\n\tif b.Len() == 0 {\n\t\treturn\n\t}\n\tb.WriteRune('\\n')\n}\n\n// field renders a part of the log line.\ntype field func(b *bytes.Buffer, e *Event)\n\n// fields contains the known log fields and their field functions. The field\n// functions should avoid to alloc memory at all cost since they are in the hot\n// path. Do not use fmt.Sprintf() but combine the value from the parts. Instead\n// of strconv.Atoi/FormatInt() use the local atoi() function which does not\n// alloc.\nvar fields = map[string]field{\n\t\"$remote_addr\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.RemoteAddr)\n\t},\n\t\"$remote_host\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\thost, _ := hostport(e.Request.RemoteAddr)\n\t\tb.WriteString(host)\n\t},\n\t\"$remote_port\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\t_, port := hostport(e.Request.RemoteAddr)\n\t\tb.WriteString(port)\n\t},\n\t\"$request\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.Method)\n\t\tb.WriteRune(' ')\n\t\tb.WriteString(e.Request.RequestURI)\n\t\tb.WriteRune(' ')\n\t\tb.WriteString(e.Request.Proto)\n\t},\n\t\"$request_args\": func(b *bytes.Buffer, e *Event) {\n\t\t// cannot use e.Req.URL since it may have been modified\n\t\tif e.RequestURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.RequestURL.RawQuery)\n\t},\n\t\"$request_host\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.Host)\n\t},\n\t\"$request_method\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.Method)\n\t},\n\t\"$request_scheme\": func(b *bytes.Buffer, e *Event) {\n\t\t// cannot use e.Req.URL since it may have been modified\n\t\tif e.RequestURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.RequestURL.Scheme)\n\t},\n\t\"$request_uri\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.RequestURI)\n\t},\n\t\"$request_url\": func(b *bytes.Buffer, e *Event) {\n\t\t// cannot use e.Req.URL since it may have been modified\n\t\tif e.RequestURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.RequestURL.String())\n\t},\n\t\"$request_proto\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.Request == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.Request.Proto)\n\t},\n\t\"$response_body_size\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, e.Response.ContentLength, 0)\n\t},\n\t\"$response_status\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.Response.StatusCode), 0)\n\t},\n\t\"$response_time_ms\": func(b *bytes.Buffer, e *Event) {\n\t\td := e.End.Sub(e.Start).Nanoseconds()\n\t\ts, us := d/int64(time.Second), d%int64(time.Second)/int64(time.Millisecond)\n\t\tatoi(b, s, 0)\n\t\tb.WriteRune('.')\n\t\tatoi(b, us, 3)\n\t},\n\t\"$response_time_us\": func(b *bytes.Buffer, e *Event) {\n\t\td := e.End.Sub(e.Start).Nanoseconds()\n\t\ts, us := d/int64(time.Second), d%int64(time.Second)/int64(time.Microsecond)\n\t\tatoi(b, s, 0)\n\t\tb.WriteRune('.')\n\t\tatoi(b, us, 6)\n\t},\n\t\"$response_time_ns\": func(b *bytes.Buffer, e *Event) {\n\t\td := e.End.Sub(e.Start).Nanoseconds()\n\t\ts, ns := d/int64(time.Second), d%int64(time.Second)/int64(time.Nanosecond)\n\t\tatoi(b, s, 0)\n\t\tb.WriteRune('.')\n\t\tatoi(b, ns, 9)\n\t},\n\t\"$time_unix_ms\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, e.End.UnixNano()/int64(time.Millisecond), 0)\n\t},\n\t\"$time_unix_us\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, e.End.UnixNano()/int64(time.Microsecond), 0)\n\t},\n\t\"$time_unix_ns\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, e.End.UnixNano(), 0)\n\t},\n\t\"$time_common\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.End.Day()), 2)\n\t\tb.WriteRune('/')\n\t\tb.WriteString(shortMonthNames[e.End.Month()])\n\t\tb.WriteRune('/')\n\t\tatoi(b, int64(e.End.Year()), 4)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Hour()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Minute()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Second()), 2)\n\t\tb.WriteString(\" +0000\") // TODO(fs): local time\n\t},\n\t\"$time_rfc3339\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.End.Year()), 4)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Month()), 2)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Day()), 2)\n\t\tb.WriteRune('T')\n\t\tatoi(b, int64(e.End.Hour()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Minute()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Second()), 2)\n\t\tb.WriteRune('Z')\n\t},\n\t\"$time_rfc3339_ms\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.End.Year()), 4)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Month()), 2)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Day()), 2)\n\t\tb.WriteRune('T')\n\t\tatoi(b, int64(e.End.Hour()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Minute()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Second()), 2)\n\t\tb.WriteRune('.')\n\t\tatoi(b, int64(e.End.Nanosecond())/int64(time.Millisecond), 3)\n\t\tb.WriteRune('Z')\n\t},\n\t\"$time_rfc3339_us\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.End.Year()), 4)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Month()), 2)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Day()), 2)\n\t\tb.WriteRune('T')\n\t\tatoi(b, int64(e.End.Hour()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Minute()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Second()), 2)\n\t\tb.WriteRune('.')\n\t\tatoi(b, int64(e.End.Nanosecond())/int64(time.Microsecond), 6)\n\t\tb.WriteRune('Z')\n\t},\n\t\"$time_rfc3339_ns\": func(b *bytes.Buffer, e *Event) {\n\t\tatoi(b, int64(e.End.Year()), 4)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Month()), 2)\n\t\tb.WriteRune('-')\n\t\tatoi(b, int64(e.End.Day()), 2)\n\t\tb.WriteRune('T')\n\t\tatoi(b, int64(e.End.Hour()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Minute()), 2)\n\t\tb.WriteRune(':')\n\t\tatoi(b, int64(e.End.Second()), 2)\n\t\tb.WriteRune('.')\n\t\tatoi(b, int64(e.End.Nanosecond()), 9)\n\t\tb.WriteRune('Z')\n\t},\n\t\"$upstream_addr\": func(b *bytes.Buffer, e *Event) {\n\t\tb.WriteString(e.UpstreamAddr)\n\t},\n\t\"$upstream_host\": func(b *bytes.Buffer, e *Event) {\n\t\thost, _ := hostport(e.UpstreamAddr)\n\t\tb.WriteString(host)\n\t},\n\t\"$upstream_port\": func(b *bytes.Buffer, e *Event) {\n\t\t_, port := hostport(e.UpstreamAddr)\n\t\tb.WriteString(port)\n\t},\n\t\"$upstream_request_scheme\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.UpstreamURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.UpstreamURL.Scheme)\n\t},\n\t\"$upstream_request_uri\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.UpstreamURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.UpstreamURL.RequestURI())\n\t},\n\t\"$upstream_request_url\": func(b *bytes.Buffer, e *Event) {\n\t\tif e.UpstreamURL == nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(e.UpstreamURL.String())\n\t},\n\t\"$upstream_service\": func(b *bytes.Buffer, e *Event) {\n\t\tb.WriteString(e.UpstreamService)\n\t},\n}\n\nvar shortMonthNames = []string{\n\t\"---\",\n\t\"Jan\",\n\t\"Feb\",\n\t\"Mar\",\n\t\"Apr\",\n\t\"May\",\n\t\"Jun\",\n\t\"Jul\",\n\t\"Aug\",\n\t\"Sep\",\n\t\"Oct\",\n\t\"Nov\",\n\t\"Dec\",\n}\n\n// hostport is a simplified no-alloc version of\n// net.SplitHostPort. Since we know that the\n// address values have the correct form we can\n// skip all the error checking.\nfunc hostport(s string) (host, port string) {\n\tif s == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\tn := strings.LastIndexByte(s, ':')\n\treturn s[:n], s[n+1:]\n}\n\n// atoi is a replacement for strconv.Atoi/strconv.FormatInt\n// which does not alloc.\nfunc atoi(b *bytes.Buffer, i int64, pad int) {\n\tvar flag bool\n\tif i < 0 {\n\t\tflag = true\n\t\ti = -i\n\t}\n\n\t// format number\n\t// 2^63-1 == 9223372036854775807\n\tvar d [128]byte\n\tn, p := len(d), len(d)-1\n\tfor i >= 0 {\n\t\td[p] = byte('0') + byte(i%10)\n\t\ti /= 10\n\t\tp--\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// padding\n\tfor n-p-1 < pad {\n\t\td[p] = byte('0')\n\t\tp--\n\t}\n\n\tif flag {\n\t\td[p] = '-'\n\t\tp--\n\t}\n\tb.Write(d[p+1:])\n}\n\n// parse parses a format string into a pattern based on the following rules:\n//\n// The format string consists of text and fields. Field names start with a '$'\n// and consist of ASCII characters [a-zA-Z0-9.-_]. Field names like\n// '$header.name' will render the HTTP header 'name'. All other field names\n// must exist in the fields map.\nfunc parse(format string, fields map[string]field) (p pattern, err error) {\n\t// text is a helper to add raw text to the log output.\n\ttext := func(s string) field {\n\t\treturn func(b *bytes.Buffer, e *Event) {\n\t\t\tb.WriteString(s)\n\t\t}\n\t}\n\n\t// header is a helper to add an HTTP header to the log output.\n\theader := func(name string) field {\n\t\treturn func(b *bytes.Buffer, e *Event) {\n\t\t\tif e.Request == nil || e.Request.Header == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tb.WriteString(e.Request.Header.Get(name))\n\t\t}\n\t}\n\n\ts := []rune(format)\n\tfor len(s) > 0 {\n\t\ttyp, n := lex(s)\n\t\tval := string(s[:n])\n\t\ts = s[n:]\n\t\tswitch typ {\n\t\tcase itemText:\n\t\t\tp = append(p, text(val))\n\t\tcase itemHeader:\n\t\t\tp = append(p, header(val[len(\"$header.\"):]))\n\t\tcase itemField:\n\t\t\tf := fields[val]\n\t\t\tif f == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid field %q\", val)\n\t\t\t}\n\t\t\tp = append(p, f)\n\t\t}\n\t}\n\treturn p, nil\n}\n\ntype itemType int\n\nconst (\n\titemText itemType = iota\n\titemField\n\titemHeader\n)\n\nfunc (t itemType) String() string {\n\tswitch t {\n\tcase itemText:\n\t\treturn \"TEXT\"\n\tcase itemField:\n\t\treturn \"FIELD\"\n\tcase itemHeader:\n\t\treturn \"HEADER\"\n\t}\n\tpanic(\"invalid\")\n}\n\ntype state int\n\nconst (\n\tstateStart state = iota\n\tstateText\n\tstateDollar\n\tstateField\n\tstateDot\n\tstateHeader\n)\n\nfunc lex(s []rune) (typ itemType, n int) {\n\tisIDChar := func(r rune) bool {\n\t\treturn 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || '0' <= r && r <= '9' || r == '_' || r == '-'\n\t}\n\n\tstate := stateStart\n\tfor i, r := range s {\n\t\tswitch state {\n\t\tcase stateStart:\n\t\t\tswitch r {\n\t\t\tcase '$':\n\t\t\t\tstate = stateDollar\n\t\t\tdefault:\n\t\t\t\tstate = stateText\n\t\t\t}\n\n\t\tcase stateText:\n\t\t\tswitch r {\n\t\t\tcase '$':\n\t\t\t\treturn itemText, i\n\t\t\tdefault:\n\t\t\t\t// state = stateText\n\t\t\t}\n\n\t\tcase stateDollar:\n\t\t\tswitch {\n\t\t\tcase isIDChar(r):\n\t\t\t\tstate = stateField\n\t\t\tdefault:\n\t\t\t\tstate = stateText\n\t\t\t}\n\n\t\tcase stateField:\n\t\t\tswitch {\n\t\t\tcase r == '.':\n\t\t\t\tif string(s[:i]) == \"$header\" {\n\t\t\t\t\tstate = stateDot\n\t\t\t\t} else {\n\t\t\t\t\treturn itemField, i\n\t\t\t\t}\n\t\t\tcase isIDChar(r):\n\t\t\t\t// state = stateField\n\t\t\tdefault:\n\t\t\t\treturn itemField, i\n\t\t\t}\n\n\t\tcase stateDot:\n\t\t\tswitch {\n\t\t\tcase isIDChar(r):\n\t\t\t\tstate = stateHeader\n\t\t\tdefault:\n\t\t\t\treturn itemField, i\n\t\t\t}\n\n\t\tcase stateHeader:\n\t\t\tswitch {\n\t\t\tcase isIDChar(r):\n\t\t\t\t// state = stateHeader\n\t\t\tdefault:\n\t\t\t\treturn itemHeader, i\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch state {\n\tcase stateDot:\n\t\treturn itemField, len(s) - 1\n\tcase stateField:\n\t\treturn itemField, len(s)\n\tcase stateHeader:\n\t\treturn itemHeader, len(s)\n\tdefault:\n\t\treturn itemText, len(s)\n\t}\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/bgp\"\n\t\"github.com/fabiolb/fabio/transport\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\n\t\"github.com/fabiolb/fabio/admin\"\n\t\"github.com/fabiolb/fabio/auth\"\n\t\"github.com/fabiolb/fabio/cert\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/exit\"\n\t\"github.com/fabiolb/fabio/logger\"\n\t\"github.com/fabiolb/fabio/metrics\"\n\t\"github.com/fabiolb/fabio/noroute\"\n\t\"github.com/fabiolb/fabio/proxy\"\n\t\"github.com/fabiolb/fabio/proxy/tcp\"\n\t\"github.com/fabiolb/fabio/registry\"\n\t\"github.com/fabiolb/fabio/registry/consul\"\n\t\"github.com/fabiolb/fabio/registry/custom\"\n\t\"github.com/fabiolb/fabio/registry/file\"\n\t\"github.com/fabiolb/fabio/registry/static\"\n\t\"github.com/fabiolb/fabio/route\"\n\n\tgrpc_proxy \"github.com/mwitkow/grpc-proxy/proxy\"\n\t\"github.com/pkg/profile\"\n\tdmp \"github.com/sergi/go-diff/diffmatchpatch\"\n\t\"google.golang.org/grpc\"\n)\n\n// version contains the version number\n//\n// It is set by build/release.sh for tagged releases\n// so that 'go get' just works.\n//\n// It is also set by the linker when fabio\n// is built via the Makefile or the build/docker.sh\n// script to ensure the correct version number\nvar version = \"1.6.11\"\n\nvar shuttingDown int32\n\nfunc main() {\n\tlogOutput := logger.NewLevelWriter(os.Stderr, \"INFO\", \"2017/01/01 00:00:00 \")\n\tlog.SetOutput(logOutput)\n\n\tcfg, err := config.Load(os.Args, os.Environ())\n\tif err != nil {\n\t\texit.Fatalf(\"[FATAL] %s. %s\", version, err)\n\t}\n\tif cfg == nil {\n\t\tfmt.Printf(\"%s %s\\n\", version, runtime.Version())\n\t\treturn\n\t}\n\n\ttransport.SetConfig(cfg)\n\n\tlog.Printf(\"[INFO] Setting log level to %s\", logOutput.Level())\n\tif !logOutput.SetLevel(cfg.Log.Level) {\n\t\tlog.Printf(\"[INFO] Cannot set log level to %s\", cfg.Log.Level)\n\t}\n\n\tlog.Printf(\"%s\", \"[INFO] Runtime config\\n\"+toJSON(cfg))\n\tlog.Printf(\"[INFO] Version %s starting\", version)\n\tlog.Printf(\"[INFO] Go runtime is %s\", runtime.Version())\n\n\t// warn once so that it is at the beginning of the log\n\t// this will also start the reminder go routine if necessary.\n\tWarnIfRunAsRoot(cfg.Insecure)\n\n\t// setup profiling if enabled\n\tvar prof interface {\n\t\tStop()\n\t}\n\tif cfg.ProfileMode != \"\" {\n\t\tvar mode func(*profile.Profile)\n\t\tswitch cfg.ProfileMode {\n\t\tcase \"\":\n\t\t\t// do nothing\n\t\tcase \"cpu\":\n\t\t\tmode = profile.CPUProfile\n\t\tcase \"mem\":\n\t\t\tmode = profile.MemProfile\n\t\tcase \"mutex\":\n\t\t\tmode = profile.MutexProfile\n\t\tcase \"block\":\n\t\t\tmode = profile.BlockProfile\n\t\tcase \"trace\":\n\t\t\tmode = profile.TraceProfile\n\t\tdefault:\n\t\t\tlog.Fatalf(\"[FATAL] Invalid profile mode %q\", cfg.ProfileMode)\n\t\t}\n\n\t\tprof = profile.Start(mode, profile.ProfilePath(cfg.ProfilePath), profile.NoShutdownHook)\n\t\tlog.Printf(\"[INFO] Profile mode %q\", cfg.ProfileMode)\n\t\tlog.Printf(\"[INFO] Profile path %q\", cfg.ProfilePath)\n\t}\n\n\tif cfg.BGP.BGPEnabled {\n\t\terr = bgp.ValidateConfig(&cfg.BGP)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"[FATAL] BGP configuration invalid: %s\", err)\n\t\t}\n\t}\n\n\texit.Listen(func(s os.Signal) {\n\t\tatomic.StoreInt32(&shuttingDown, 1)\n\t\tif registry.Default != nil {\n\t\t\tregistry.Default.DeregisterAll()\n\t\t}\n\t\ttime.Sleep(cfg.Proxy.DeregisterGracePeriod)\n\t\tproxy.Shutdown(cfg.Proxy.ShutdownWait)\n\t\tif prof != nil {\n\t\t\tprof.Stop()\n\t\t}\n\t})\n\n\tmetrics, err := metrics.Initialize(&cfg.Metrics)\n\tif err != nil {\n\t\texit.Fatal(\"[FATAL] \", err)\n\t}\n\troute.SetMetricsProvider(metrics)\n\tinitRuntime(cfg)\n\tinitBackend(cfg)\n\n\tstartAdmin(cfg)\n\n\tgo watchNoRouteHTML()\n\n\tfirst := make(chan bool)\n\tgo watchBackend(cfg, first)\n\tlog.Print(\"[INFO] Waiting for first routing table\")\n\t<-first\n\n\t// create proxies after metrics since they use the metrics registry.\n\tstartServers(cfg, metrics)\n\n\t// warn again so that it is visible in the terminal\n\tWarnIfRunAsRoot(cfg.Insecure)\n\tif cfg.BGP.BGPEnabled {\n\t\tstartBGP(&cfg.BGP)\n\t}\n\texit.Wait()\n\tlog.Print(\"[INFO] Down\")\n}\n\nfunc newGrpcProxy(cfg *config.Config, tlscfg *tls.Config, statsHandler *proxy.GrpcStatsHandler) []grpc.ServerOption {\n\n\t//Init Glob Cache\n\tglobCache := route.NewGlobCache(cfg.GlobCacheSize)\n\n\tproxyInterceptor := proxy.GrpcProxyInterceptor{\n\t\tConfig:       cfg,\n\t\tStatsHandler: statsHandler,\n\t\tGlobCache:    globCache,\n\t}\n\n\thandler := grpc_proxy.TransparentHandler(proxy.GetGRPCDirector(tlscfg, cfg))\n\n\treturn []grpc.ServerOption{\n\t\tgrpc.UnknownServiceHandler(handler),\n\t\tgrpc.StreamInterceptor(proxyInterceptor.Stream),\n\t\tgrpc.StatsHandler(statsHandler),\n\t\tgrpc.MaxRecvMsgSize(cfg.Proxy.GRPCMaxRxMsgSize),\n\t\tgrpc.MaxSendMsgSize(cfg.Proxy.GRPCMaxTxMsgSize),\n\t}\n}\n\nfunc newHTTPProxy(cfg *config.Config, statsHandler *proxy.HttpStatsHandler) *proxy.HTTPProxy {\n\tvar w io.Writer\n\n\t//Init Glob Cache\n\tglobCache := route.NewGlobCache(cfg.GlobCacheSize)\n\n\tswitch cfg.Log.AccessTarget {\n\tcase \"\":\n\t\tlog.Printf(\"[INFO] Access logging disabled\")\n\tcase \"stdout\":\n\t\tlog.Printf(\"[INFO] Writing access log to stdout\")\n\t\tw = os.Stdout\n\tdefault:\n\t\texit.Fatal(\"[FATAL] Invalid access log target \", cfg.Log.AccessTarget)\n\t}\n\n\tformat := cfg.Log.AccessFormat\n\tswitch format {\n\tcase \"common\":\n\t\tformat = logger.CommonFormat\n\tcase \"combined\":\n\t\tformat = logger.CombinedFormat\n\t}\n\n\tl, err := logger.New(w, format)\n\tif err != nil {\n\t\texit.Fatal(\"[FATAL] Invalid log format: \", err)\n\t}\n\n\tpick := route.Picker[cfg.Proxy.Strategy]\n\tmatch := route.Matcher[cfg.Proxy.Matcher]\n\tlog.Printf(\"[INFO] Using routing strategy %q\", cfg.Proxy.Strategy)\n\tlog.Printf(\"[INFO] Using route matching %q\", cfg.Proxy.Matcher)\n\n\tauthSchemes, err := auth.LoadAuthSchemes(cfg.Proxy.AuthSchemes)\n\n\tif err != nil {\n\t\texit.Fatal(\"[FATAL] \", err)\n\t}\n\n\treturn &proxy.HTTPProxy{\n\t\tConfig:            cfg.Proxy,\n\t\tTransport:         transport.NewTransport(nil),\n\t\tInsecureTransport: transport.NewTransport(&tls.Config{InsecureSkipVerify: true}),\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\tt := route.GetTable().Lookup(r, pick, match, globCache, cfg.GlobMatchingDisabled)\n\t\t\tif t == nil {\n\t\t\t\tstatsHandler.Noroute.Add(1)\n\t\t\t\tlog.Print(\"[WARN] No route for \", r.Host, r.URL)\n\t\t\t}\n\t\t\treturn t\n\t\t},\n\t\tLogger:      l,\n\t\tAuthSchemes: authSchemes,\n\t\tStats:       *statsHandler,\n\t}\n}\n\nfunc lookupHostFn(cfg *config.Config, notFound gkm.Counter) func(string) *route.Target {\n\tpick := route.Picker[cfg.Proxy.Strategy]\n\treturn func(host string) *route.Target {\n\t\tt := route.GetTable().LookupHost(host, pick)\n\t\tif t == nil {\n\t\t\tnotFound.Add(1)\n\t\t\tlog.Print(\"[WARN] No route for \", host)\n\t\t}\n\t\treturn t\n\t}\n}\n\n// Returns a matcher function compatible with tcpproxy Matcher from github.com/inetaf/tcpproxy\nfunc lookupHostMatcher(cfg *config.Config) func(context.Context, string) bool {\n\tpick := route.Picker[cfg.Proxy.Strategy]\n\treturn func(ctx context.Context, host string) bool {\n\t\tt := route.GetTable().LookupHost(host, pick)\n\t\tif t == nil {\n\t\t\treturn false\n\t\t}\n\n\t\t// Make sure this is supposed to be a tcp proxy.\n\t\t// opts proto= overrides scheme if present.\n\t\tvar (\n\t\t\tok    bool\n\t\t\tproto string\n\t\t)\n\t\tif proto, ok = t.Opts[\"proto\"]; !ok && t.URL != nil {\n\t\t\tproto = t.URL.Scheme\n\t\t}\n\t\treturn proto == \"tcp\"\n\t}\n}\n\nfunc makeTLSConfig(l config.Listen) (*tls.Config, error) {\n\tif l.CertSource.Name == \"\" {\n\t\treturn nil, nil\n\t}\n\tsrc, err := cert.NewSource(l.CertSource)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create cert source %s. %s\", l.CertSource.Name, err)\n\t}\n\ttlscfg, err := cert.TLSConfig(src, l.StrictMatch, l.TLSMinVersion, l.TLSMaxVersion, l.TLSCiphers)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[FATAL] Failed to create TLS config for cert source %s. %s\", l.CertSource.Name, err)\n\t}\n\treturn tlscfg, nil\n}\n\nfunc startAdmin(cfg *config.Config) {\n\tlog.Printf(\"[INFO] Admin server access mode %q\", cfg.UI.Access)\n\tlog.Printf(\"[INFO] Admin server listening on %q\", cfg.UI.Listen.Addr)\n\tgo func() {\n\t\tl := cfg.UI.Listen\n\t\ttlscfg, err := makeTLSConfig(l)\n\t\tif err != nil {\n\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t}\n\t\tsrv := &admin.Server{\n\t\t\tAccess:   cfg.UI.Access,\n\t\t\tColor:    cfg.UI.Color,\n\t\t\tTitle:    cfg.UI.Title,\n\t\t\tVersion:  version,\n\t\t\tCommands: route.Commands,\n\t\t\tCfg:      cfg,\n\t\t}\n\t\tif err := srv.ListenAndServe(l, tlscfg); err != nil {\n\t\t\texit.Fatal(\"[FATAL] ui: \", err)\n\t\t}\n\t}()\n}\n\nfunc startServers(cfg *config.Config, stats metrics.Provider) {\n\tnotFound := stats.NewCounter(\"notfound\")\n\n\tvar (\n\t\ttcpConn          gkm.Counter\n\t\ttcpConnFail      gkm.Counter\n\t\ttcpNoRoute       gkm.Counter\n\t\ttcpSniConn       gkm.Counter\n\t\ttcpSniConnFail   gkm.Counter\n\t\ttcpSniNoRoute    gkm.Counter\n\t\tgrpStatsHandler  *proxy.GrpcStatsHandler\n\t\thttpStatsHandler *proxy.HttpStatsHandler\n\t)\n\n\tgrpcCounters := func() {\n\t\tgrpStatsHandler = &proxy.GrpcStatsHandler{\n\t\t\tConnect: stats.NewCounter(\"grpc.conn\"),\n\t\t\tRequest: stats.NewHistogram(\"grpc.requests\"),\n\t\t\tNoRoute: stats.NewCounter(\"grpc.noroute\"),\n\t\t\tStatus:  stats.NewHistogram(\"grep.status\", \"code\"),\n\t\t}\n\t}\n\n\tvar grpcOnce sync.Once\n\n\thttpCounters := func() {\n\t\thttpStatsHandler = &proxy.HttpStatsHandler{\n\t\t\tRequests:        stats.NewHistogram(\"requests\"),\n\t\t\tNoroute:         notFound,\n\t\t\tWSConn:          stats.NewGauge(\"ws.conn\"),\n\t\t\tStatusTimer:     stats.NewHistogram(\"http.status\", \"code\"),\n\t\t\tRedirectCounter: stats.NewCounter(\"http.redirect.count\", \"code\"),\n\t\t}\n\t}\n\n\tvar httpOnce sync.Once\n\n\ttcpCounters := func() {\n\t\ttcpConn = stats.NewCounter(\"tcp.conn\")\n\t\ttcpConnFail = stats.NewCounter(\"tcp.connfail\")\n\t\ttcpNoRoute = stats.NewCounter(\"tcp.noroute\")\n\t}\n\tvar tcpOnce sync.Once\n\n\ttcpSniCounters := func() {\n\t\ttcpSniConn = stats.NewCounter(\"tcp_sni.conn\")\n\t\ttcpSniConnFail = stats.NewCounter(\"tcp_sni.connfail\")\n\t\ttcpSniNoRoute = stats.NewCounter(\"tcp_sni.noroute\")\n\t}\n\n\tvar tcpSniOnce sync.Once\n\n\tfor _, l := range cfg.Listen {\n\t\tl := l // capture loop var for go routines below\n\t\ttlscfg, err := makeTLSConfig(l)\n\t\tif err != nil {\n\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t}\n\n\t\tlog.Printf(\"[INFO] %s proxy listening on %s\", strings.ToUpper(l.Proto), l.Addr)\n\t\tif tlscfg != nil && tlscfg.ClientAuth == tls.RequireAndVerifyClientCert {\n\t\t\tlog.Printf(\"[INFO] Client certificate authentication enabled on %s\", l.Addr)\n\t\t}\n\t\tswitch l.Proto {\n\t\tcase \"http\", \"https\":\n\t\t\thttpOnce.Do(httpCounters)\n\t\t\tgo func() {\n\t\t\t\th := newHTTPProxy(cfg, httpStatsHandler)\n\t\t\t\t// reset the ws.conn gauge\n\t\t\t\th.Stats.WSConn.Set(0)\n\t\t\t\tif err := proxy.ListenAndServeHTTP(l, h, tlscfg); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"grpc\", \"grpcs\":\n\t\t\tgrpcOnce.Do(grpcCounters)\n\t\t\tgo func() {\n\t\t\t\th := newGrpcProxy(cfg, tlscfg, grpStatsHandler)\n\t\t\t\tif err := proxy.ListenAndServeGRPC(l, h, tlscfg); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"tcp\":\n\t\t\ttcpOnce.Do(tcpCounters)\n\t\t\tgo func() {\n\t\t\t\th := &tcp.Proxy{\n\t\t\t\t\tDialTimeout: cfg.Proxy.DialTimeout,\n\t\t\t\t\tLookup:      lookupHostFn(cfg, notFound),\n\t\t\t\t\tConn:        tcpConn,\n\t\t\t\t\tConnFail:    tcpConnFail,\n\t\t\t\t\tNoroute:     tcpNoRoute,\n\t\t\t\t}\n\t\t\t\tif err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"tcp+sni\":\n\t\t\ttcpSniOnce.Do(tcpSniCounters)\n\t\t\tgo func() {\n\t\t\t\th := &tcp.SNIProxy{\n\t\t\t\t\tDialTimeout: cfg.Proxy.DialTimeout,\n\t\t\t\t\tLookup:      lookupHostFn(cfg, notFound),\n\t\t\t\t\tConn:        tcpSniConn,\n\t\t\t\t\tConnFail:    tcpSniConnFail,\n\t\t\t\t\tNoroute:     tcpSniNoRoute,\n\t\t\t\t}\n\t\t\t\tif err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"tcp-dynamic\":\n\t\t\ttcpOnce.Do(tcpCounters)\n\t\t\tgo func() {\n\t\t\t\tvar buffer strings.Builder\n\t\t\t\tlastPorts := []string{}\n\t\t\t\tfor {\n\t\t\t\t\ttime.Sleep(l.Refresh)\n\t\t\t\t\ttable := route.GetTable()\n\t\t\t\t\tports := []string{}\n\t\t\t\t\tfor target, rts := range table {\n\t\t\t\t\t\tif strings.Contains(target, \":\") {\n\t\t\t\t\t\t\tbuffer.WriteString(\":\")\n\t\t\t\t\t\t\tbuffer.WriteString(strings.Split(target, \":\")[1])\n\n\t\t\t\t\t\t\tschemes := tableSchemes(rts)\n\t\t\t\t\t\t\tif len(schemes) == 1 && schemes[0] == \"tcp\" {\n\t\t\t\t\t\t\t\tports = append(ports, buffer.String())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbuffer.Reset()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tports = unique(ports)\n\t\t\t\t\t}\n\t\t\t\t\tfor _, port := range difference(lastPorts, ports) {\n\t\t\t\t\t\tlog.Printf(\"[DEBUG] Dynamic TCP listener on %s eligible for termination\", port)\n\t\t\t\t\t\tproxy.CloseProxy(port)\n\t\t\t\t\t}\n\t\t\t\t\tfor _, port := range ports {\n\t\t\t\t\t\tl := l\n\t\t\t\t\t\tport := port\n\t\t\t\t\t\tconn, err := net.Listen(\"tcp\", port)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Printf(\"[DEBUG] Dynamic TCP port %s in use\", port)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconn.Close()\n\t\t\t\t\t\tlog.Printf(\"[INFO] Starting dynamic TCP listener on port %s \", port)\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\th := &tcp.DynamicProxy{\n\t\t\t\t\t\t\t\tDialTimeout: cfg.Proxy.DialTimeout,\n\t\t\t\t\t\t\t\tLookup:      lookupHostFn(cfg, notFound),\n\t\t\t\t\t\t\t\tConn:        tcpConn,\n\t\t\t\t\t\t\t\tConnFail:    tcpConnFail,\n\t\t\t\t\t\t\t\tNoroute:     tcpNoRoute,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tl.Addr = port\n\t\t\t\t\t\t\tif err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil {\n\t\t\t\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}()\n\t\t\t\t\t}\n\t\t\t\t\tlastPorts = ports\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"https+tcp+sni\":\n\t\t\ttcpSniOnce.Do(tcpSniCounters)\n\t\t\thttpOnce.Do(httpCounters)\n\t\t\tgo func() {\n\t\t\t\thp := newHTTPProxy(cfg, httpStatsHandler)\n\t\t\t\ttp := &tcp.SNIProxy{\n\t\t\t\t\tDialTimeout: cfg.Proxy.DialTimeout,\n\t\t\t\t\tLookup:      lookupHostFn(cfg, notFound),\n\t\t\t\t\tConn:        tcpSniConn,\n\t\t\t\t\tConnFail:    tcpSniConnFail,\n\t\t\t\t\tNoroute:     tcpSniNoRoute}\n\t\t\t\tif err := proxy.ListenAndServeHTTPSTCPSNI(l, hp, tp, tlscfg, lookupHostMatcher(cfg)); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tcase \"prometheus\":\n\t\t\tgo func() {\n\t\t\t\tif err := proxy.ListenAndServePrometheus(l, cfg.Metrics.Prometheus, tlscfg); err != nil {\n\t\t\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t\t\t}\n\t\t\t}()\n\t\tdefault:\n\t\t\texit.Fatal(\"[FATAL] Invalid protocol \", l.Proto)\n\t\t}\n\t}\n}\n\nfunc startBGP(cfg *config.BGP) {\n\th, err := bgp.NewBGPHandler(cfg)\n\tif err != nil {\n\t\texit.Fatal(\"[FATAL] \", err)\n\t}\n\tgo func() {\n\t\tif err := h.Start(); err != nil {\n\t\t\texit.Fatal(\"[FATAL] \", err)\n\t\t}\n\t}()\n}\n\nfunc initRuntime(cfg *config.Config) {\n\tif os.Getenv(\"GOGC\") == \"\" {\n\t\tlog.Print(\"[INFO] Setting GOGC=\", cfg.Runtime.GOGC)\n\t\tdebug.SetGCPercent(cfg.Runtime.GOGC)\n\t} else {\n\t\tlog.Print(\"[INFO] Using GOGC=\", os.Getenv(\"GOGC\"), \" from env\")\n\t}\n\n\tif os.Getenv(\"GOMAXPROCS\") == \"\" {\n\t\tlog.Print(\"[INFO] Setting GOMAXPROCS=\", cfg.Runtime.GOMAXPROCS)\n\t\truntime.GOMAXPROCS(cfg.Runtime.GOMAXPROCS)\n\t} else {\n\t\tlog.Print(\"[INFO] Using GOMAXPROCS=\", os.Getenv(\"GOMAXPROCS\"), \" from env\")\n\t}\n}\n\nfunc initBackend(cfg *config.Config) {\n\tvar deadline = time.Now().Add(cfg.Registry.Timeout)\n\tvar err error\n\tfor {\n\t\tswitch cfg.Registry.Backend {\n\t\tcase \"file\":\n\t\t\tregistry.Default, err = file.NewBackend(&cfg.Registry.File)\n\t\tcase \"static\":\n\t\t\tregistry.Default, err = static.NewBackend(&cfg.Registry.Static)\n\t\tcase \"consul\":\n\t\t\tregistry.Default, err = consul.NewBackend(&cfg.Registry.Consul)\n\t\tcase \"custom\":\n\t\t\tregistry.Default, err = custom.NewBackend(&cfg.Registry.Custom)\n\t\tdefault:\n\t\t\texit.Fatal(\"[FATAL] Unknown registry backend \", cfg.Registry.Backend)\n\t\t}\n\n\t\tif err == nil {\n\t\t\tif err = registry.Default.Register(nil); err == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tlog.Print(\"[WARN] Error initializing backend. \", err)\n\n\t\tif time.Now().After(deadline) {\n\t\t\texit.Fatal(\"[FATAL] Timeout registering backend.\")\n\t\t}\n\n\t\ttime.Sleep(cfg.Registry.Retry)\n\t\tif atomic.LoadInt32(&shuttingDown) > 0 {\n\t\t\texit.Exit(1)\n\t\t}\n\t}\n}\n\nfunc watchBackend(cfg *config.Config, first chan bool) {\n\tvar (\n\t\tnextTable   string\n\t\tlastTable   string\n\t\tsvccfg      string\n\t\tmancfg      string\n\t\tcustomBE    string\n\t\tonce        sync.Once\n\t\ttableBuffer = new(bytes.Buffer) // fix crash on reset before used (#650)\n\t)\n\n\tswitch cfg.Registry.Backend {\n\t// custom back end receives JSON from a remote source that contains a slice of route.RouteDef\n\t// the route table is created directly from that input\n\tcase \"custom\":\n\t\tsvc := registry.Default.WatchServices()\n\t\tfor {\n\t\t\tcustomBE = <-svc\n\t\t\tif customBE != \"OK\" {\n\t\t\t\tlog.Printf(\"[ERROR] error during update from custom back end - %s\", customBE)\n\t\t\t}\n\t\t\tonce.Do(func() { close(first) })\n\t\t}\n\t// all other backend types\n\tdefault:\n\t\tsvc := registry.Default.WatchServices()\n\t\tman := registry.Default.WatchManual()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase svccfg = <-svc:\n\t\t\tcase mancfg = <-man:\n\t\t\t}\n\t\t\t// manual config overrides service config - order matters\n\t\t\ttableBuffer.Reset()\n\t\t\ttableBuffer.WriteString(svccfg)\n\t\t\ttableBuffer.WriteString(\"\\n\")\n\t\t\ttableBuffer.WriteString(mancfg)\n\t\t\t// set nextTable here to preserve the state.  The buffer is altered\n\t\t\t// when calling route.NewTable and we lose change logging (#737)\n\t\t\tif nextTable = tableBuffer.String(); nextTable == lastTable {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\taliases, err := route.ParseAliases(nextTable)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[WARN]: %s\", err)\n\t\t\t}\n\t\t\tregistry.Default.Register(aliases)\n\t\t\tt, err := route.NewTable(tableBuffer)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[WARN] %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\troute.SetTable(t)\n\t\t\tlogRoutes(t, lastTable, nextTable, cfg.Log.RoutesFormat)\n\t\t\tlastTable = nextTable\n\t\t\tonce.Do(func() { close(first) })\n\t\t}\n\t}\n}\n\nfunc watchNoRouteHTML() {\n\thtml := registry.Default.WatchNoRouteHTML()\n\tfor {\n\t\tnext := <-html\n\t\tif next == noroute.GetHTML() {\n\t\t\tcontinue\n\t\t}\n\t\tnoroute.SetHTML(next)\n\t\tif next == \"\" {\n\t\t\tlog.Print(\"[INFO] Unset noroute HTML\")\n\t\t} else {\n\t\t\tlog.Printf(\"[INFO] Set noroute HTML (%d bytes)\", len(next))\n\t\t}\n\t}\n}\n\nfunc logRoutes(t route.Table, last, next, format string) {\n\tfmtDiff := func(diffs []dmp.Diff) string {\n\t\tvar b bytes.Buffer\n\t\tfor _, d := range diffs {\n\t\t\tt := strings.TrimSpace(d.Text)\n\t\t\tif t == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch d.Type {\n\t\t\tcase dmp.DiffDelete:\n\t\t\t\tb.WriteString(\"- \")\n\t\t\t\tb.WriteString(strings.ReplaceAll(t, \"\\n\", \"\\n- \"))\n\t\t\tcase dmp.DiffInsert:\n\t\t\t\tb.WriteString(\"+ \")\n\t\t\t\tb.WriteString(strings.ReplaceAll(t, \"\\n\", \"\\n+ \"))\n\t\t\t}\n\t\t}\n\t\treturn b.String()\n\t}\n\n\tconst defFormat = \"delta\"\n\tswitch format {\n\tcase \"detail\":\n\t\tlog.Printf(\"[INFO] Updated config to\\n%s\", t.Dump())\n\n\tcase \"delta\":\n\t\tif delta := fmtDiff(dmp.New().DiffMain(last, next, true)); delta != \"\" {\n\t\t\tlog.Printf(\"[INFO] Config updates\\n%s\", delta)\n\t\t}\n\n\tcase \"all\":\n\t\tlog.Printf(\"[INFO] Updated config to\\n%s\", next)\n\n\tdefault:\n\t\tlog.Printf(\"[WARN] Invalid route format %q. Defaulting to %q\", format, defFormat)\n\t\tlogRoutes(t, last, next, defFormat)\n\t}\n}\n\nfunc toJSON(v interface{}) string {\n\tdata, err := json.MarshalIndent(v, \"\", \"    \")\n\tif err != nil {\n\t\tpanic(\"json: \" + err.Error())\n\t}\n\treturn string(data)\n}\n\nfunc unique(strSlice []string) []string {\n\tkeys := make(map[string]bool)\n\tlist := []string{}\n\tfor _, entry := range strSlice {\n\t\tif _, value := keys[entry]; !value {\n\t\t\tkeys[entry] = true\n\t\t\tlist = append(list, entry)\n\t\t}\n\t}\n\treturn list\n}\n\n// difference returns elements in `a` that aren't in `b`\nfunc difference(a, b []string) []string {\n\tmb := make(map[string]struct{}, len(b))\n\tfor _, x := range b {\n\t\tmb[x] = struct{}{}\n\t}\n\tvar diff []string\n\tfor _, x := range a {\n\t\tif _, found := mb[x]; !found {\n\t\t\tdiff = append(diff, x)\n\t\t}\n\t}\n\treturn diff\n}\n\nfunc tableSchemes(r route.Routes) []string {\n\tschemes := []string{}\n\tfor _, rt := range r {\n\t\tfor _, target := range rt.Targets {\n\t\t\tschemes = append(schemes, target.URL.Scheme)\n\t\t}\n\t}\n\treturn unique(schemes)\n}\n"
  },
  {
    "path": "metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n)\n\n// Provider is an abstraction of a metrics backend.\ntype Provider interface {\n\t// NewCounter creates a new counter object.\n\tNewCounter(name string, labels ...string) gkm.Counter\n\n\t// NewGauge creates a new gauge object.\n\tNewGauge(name string, labels ...string) gkm.Gauge\n\n\t// NewHistogram creates a new histogram object\n\tNewHistogram(name string, labels ...string) gkm.Histogram\n}\n\n// DeletableCounter is a counter that supports deleting label value combinations.\n// This is used to clean up stale metrics when routes are removed.\ntype DeletableCounter interface {\n\tgkm.Counter\n\t// DeleteLabelValues removes the metric with the given label values.\n\t// Returns true if the metric was deleted.\n\tDeleteLabelValues(labelValues ...string) bool\n}\n\n// DeletableGauge is a gauge that supports deleting label value combinations.\ntype DeletableGauge interface {\n\tgkm.Gauge\n\t// DeleteLabelValues removes the metric with the given label values.\n\tDeleteLabelValues(labelValues ...string) bool\n}\n\n// DeletableHistogram is a histogram that supports deleting label value combinations.\ntype DeletableHistogram interface {\n\tgkm.Histogram\n\t// DeleteLabelValues removes the metric with the given label values.\n\tDeleteLabelValues(labelValues ...string) bool\n}\n\nfunc Initialize(cfg *config.Metrics) (Provider, error) {\n\tvar p []Provider\n\tvar prefix string\n\tvar err error\n\tif prefix, err = parsePrefix(cfg.Prefix); err != nil {\n\t\treturn nil, fmt.Errorf(\"metrics: invalid Prefix template: %w\", err)\n\t}\n\tfor _, x := range strings.Split(cfg.Target, \",\") {\n\t\tx = strings.TrimSpace(x)\n\t\tswitch x {\n\t\tcase \"flat\", \"stdout\":\n\t\t\tp = append(p, &flatProvider{prefix})\n\t\tcase \"label\":\n\t\t\tp = append(p, &labelProvider{prefix})\n\t\tcase \"statsd_raw\":\n\t\t\tpp, err := NewStatsdProvider(prefix, cfg.StatsDAddr, cfg.Interval)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tp = append(p, pp)\n\t\tcase \"statsd\":\n\t\t\treturn nil, fmt.Errorf(\"statsd support has been removed in favor of statsd_raw\")\n\t\tcase \"dogstatsd\":\n\t\t\tpp, err := NewDogstatsdProvider(prefix, cfg.DogstatsdAddr, cfg.Interval)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tp = append(p, pp)\n\t\tcase \"prometheus\":\n\t\t\tp = append(p, NewPromProvider(prefix, cfg.Prometheus.Subsystem, cfg.Prometheus.Buckets))\n\t\tcase \"circonus\":\n\t\t\tpp, err := NewCirconusProvider(prefix, cfg.Circonus, cfg.Interval)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tp = append(p, pp)\n\t\tcase \"graphite\":\n\t\t\tpp, err := NewGraphiteProvider(prefix, cfg.GraphiteAddr, 50, cfg.Interval)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tp = append(p, pp)\n\t\tcase \"\":\n\t\t\t// metrics are disabled.\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"invalid metrics backend %s\", x)\n\t\t}\n\t\tlog.Printf(\"[INFO] Registering metrics provider %q\", x)\n\n\t\tif len(p) == 0 {\n\t\t\tlog.Printf(\"[INFO] Metrics disabled\")\n\t\t}\n\t}\n\tif len(p) == 0 {\n\t\treturn &DiscardProvider{}, nil\n\t}\n\treturn NewMultiProvider(p), nil\n}\n"
  },
  {
    "path": "metrics/names.go",
    "content": "package metrics\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\ntype Service struct {\n\tTargetURL *url.URL\n\tService   string\n\tHost      string\n\tPath      string\n}\n\n// DefaultNames contains the default template for route metric names for backends that don't\n// support tags.\nconst DefaultNames = \"{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}\"\n\n// DefaulPrefix contains the default template for metrics prefix.\nconst DefaultPrefix = \"{{clean .Hostname}}.{{clean .Exec}}\"\n\n// names stores the template for the route metric names.\nvar names *template.Template\n\nfunc init() {\n\t// make sure names is initialized to something\n\tvar err error\n\tif names, err = parseNames(DefaultNames); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (s Service) String() string {\n\treturn s.Service\n}\n\nconst DotSeparator = \".\"\nconst PipeSeparator = \"|\"\nconst RoutePrefix = \"route\"\n\nfunc Flatten(name string, labels []string, separator string) string {\n\tif len(labels) == 0 {\n\t\treturn name\n\t}\n\treturn name + separator + strings.Join(labels, separator)\n}\n\nfunc Labels(labels, values []string, stringsprefix, fieldsep, recsep string) string {\n\tif len(labels) == 0 {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\t_, _ = b.WriteString(stringsprefix)\n\tfor i := range labels {\n\t\tif i > 0 {\n\t\t\t_, _ = b.WriteString(recsep)\n\t\t}\n\t\t_, _ = b.WriteString(labels[i])\n\t\t_, _ = b.WriteString(fieldsep)\n\t\tif i < len(values) {\n\t\t\t_, _ = b.WriteString(values[i])\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// parseNames parses the route metric name template.\nfunc parseNames(tmpl string) (*template.Template, error) {\n\tfuncMap := template.FuncMap{\n\t\t\"clean\": clean,\n\t}\n\tt, err := template.New(\"names\").Funcs(funcMap).Parse(tmpl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttestURL, err := url.Parse(\"http://127.0.0.1:12345/\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, err := TargetName(\"testservice\", \"test.example.com\", \"/test\", testURL.String()); err != nil {\n\t\treturn nil, err\n\t}\n\treturn t, nil\n}\n\n// parsePrefix parses the prefix metric template\nfunc parsePrefix(tmpl string) (string, error) {\n\t// Backward compatibility condition for old metrics.prefix parameter 'default'\n\tif tmpl == \"default\" {\n\t\ttmpl = DefaultPrefix\n\t}\n\tfuncMap := template.FuncMap{\n\t\t\"clean\": clean,\n\t}\n\tt, err := template.New(\"prefix\").Funcs(funcMap).Parse(tmpl)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thost, err := hostname()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\texe := filepath.Base(os.Args[0])\n\n\tb := new(bytes.Buffer)\n\tdata := struct{ Hostname, Exec string }{host, exe}\n\tif err := t.Execute(b, &data); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn b.String(), nil\n}\n\n// clean creates safe names for graphite reporting by replacing\n// some characters with underscores.\n// TODO(fs): This may need updating for other metrics backends.\nfunc clean(s string) string {\n\tif s == \"\" {\n\t\treturn \"_\"\n\t}\n\ts = strings.ReplaceAll(s, \".\", \"_\")\n\ts = strings.ReplaceAll(s, \":\", \"_\")\n\treturn strings.ToLower(s)\n}\n\n// stubbed out for testing\nvar hostname = os.Hostname\n\n// TargetName returns the metrics name from the given parameters.\nfunc TargetName(service, host, path, target string) (string, error) {\n\tif names == nil {\n\t\treturn \"\", nil\n\t}\n\n\ttargetURL, err := url.Parse(target)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error parsing URL %s: %w\", target, err)\n\t}\n\n\tvar name bytes.Buffer\n\n\tdata := struct {\n\t\tTargetURL           *url.URL\n\t\tService, Host, Path string\n\t}{targetURL, service, host, path}\n\n\tif err := names.Execute(&name, data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn name.String(), nil\n}\n\n// TargetNameWith - this is used for flat metrics backends (no tags support)\n// in With() methods on target metrics.\nfunc TargetNameWith(name string, values []string) (string, error) {\n\n\tif len(values)%2 == 1 {\n\t\tvalues = append(values, \"unknown\")\n\t}\n\tm := make(map[string]string)\n\tfor i := 0; i < len(values); i += 2 {\n\t\tm[values[i]] = values[i+1]\n\t}\n\n\tn, err := TargetName(m[\"service\"], m[\"host\"], m[\"path\"], m[\"target\"])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Handle .tx and .rx\n\tif i := strings.LastIndex(name, \".\"); i != -1 {\n\t\tn += name[i:]\n\t}\n\treturn n, nil\n}\n\nfunc isRouteMetric(name string) bool { return strings.HasPrefix(name, RoutePrefix) }\n"
  },
  {
    "path": "metrics/names_test.go",
    "content": "package metrics\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestParsePrefix(t *testing.T) {\n\thostname = func() (string, error) { return \"myhost\", nil }\n\tos.Args = []string{\"./myapp\"}\n\tgot, err := parsePrefix(\"{{clean .Hostname}}.{{clean .Exec}}\")\n\tif err != nil {\n\t\tt.Fatalf(\"%v\", err)\n\t}\n\twant := \"myhost.myapp\"\n\tif got != want {\n\t\tt.Errorf(\"ParsePrefix: got %v want %v\", got, want)\n\t}\n\n\tgot, err = parsePrefix(\"default\")\n\tif err != nil {\n\t\tt.Fatalf(\"%v\", err)\n\t}\n\twant = \"myhost.myapp\"\n\tif got != want {\n\t\tt.Errorf(\"ParsePrefix Old default style: got %v want %v\", got, want)\n\t}\n}\n\nfunc TestTargetName(t *testing.T) {\n\ttests := []struct {\n\t\tservice, host, path, target string\n\t\tname                        string\n\t}{\n\t\t{\"s\", \"h\", \"p\", \"http://foo.com/bar\", \"s.h.p.foo_com\"},\n\t\t{\"s\", \"\", \"p\", \"http://foo.com/bar\", \"s._.p.foo_com\"},\n\t\t{\"s\", \"\", \"\", \"http://foo.com/bar\", \"s._._.foo_com\"},\n\t\t{\"\", \"\", \"\", \"http://foo.com/bar\", \"_._._.foo_com\"},\n\t\t{\"\", \"\", \"\", \"http://foo.com:1234/bar\", \"_._._.foo_com_1234\"},\n\t\t{\"\", \"\", \"\", \"http://1.2.3.4:1234/bar\", \"_._._.1_2_3_4_1234\"},\n\t}\n\n\tfor i, tt := range tests {\n\n\t\tgot, err := TargetName(tt.service, tt.host, tt.path, tt.target)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%d: %v\", i, err)\n\t\t}\n\t\tif want := tt.name; got != want {\n\t\t\tt.Errorf(\"%d: got %q want %q\", i, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "metrics/provider_circonus.go",
    "content": "package metrics\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\tcgm \"github.com/circonus-labs/circonus-gometrics/v3\"\n\t\"github.com/fabiolb/fabio/config\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n)\n\nvar (\n\tcirconus *CirconusProvider\n\tcircOnce sync.Once\n)\n\nconst serviceName = \"fabio\"\n\nfunc NewCirconusProvider(prefix string, circ config.Circonus, interval time.Duration) (*CirconusProvider, error) {\n\tvar initError error\n\n\tcircOnce.Do(func() {\n\t\tif circ.APIKey == \"\" && circ.SubmissionURL == \"\" {\n\t\t\tinitError = errors.New(\"metrics: Circonus API token key or SubmissionURL\")\n\t\t\treturn\n\t\t}\n\n\t\tif circ.APIApp == \"\" {\n\t\t\tcirc.APIApp = serviceName\n\t\t}\n\n\t\thost, err := os.Hostname()\n\t\tif err != nil {\n\t\t\tinitError = fmt.Errorf(\"metrics: unable to initialize Circonus %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\tcfg := &cgm.Config{}\n\n\t\tcfg.CheckManager.Check.SubmissionURL = circ.SubmissionURL\n\t\tcfg.CheckManager.API.TokenKey = circ.APIKey\n\t\tcfg.CheckManager.API.TokenApp = circ.APIApp\n\t\tcfg.CheckManager.API.URL = circ.APIURL\n\t\tcfg.CheckManager.Check.ID = circ.CheckID\n\t\tcfg.CheckManager.Broker.ID = circ.BrokerID\n\t\tcfg.Interval = fmt.Sprintf(\"%.0fs\", interval.Seconds())\n\t\tcfg.CheckManager.Check.InstanceID = host\n\t\tcfg.CheckManager.Check.DisplayName = fmt.Sprintf(\"%s /%s\", host, serviceName)\n\t\tcfg.CheckManager.Check.SearchTag = fmt.Sprintf(\"service:%s\", serviceName)\n\n\t\tmetrics, err := cgm.NewCirconusMetrics(cfg)\n\t\tif err != nil {\n\t\t\tinitError = fmt.Errorf(\"metrics: unable to initialize Circonus %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\tcirconus = &CirconusProvider{metrics, prefix}\n\n\t\tmetrics.Start()\n\n\t\tlog.Print(\"[INFO] Sending metrics to Circonus\")\n\t})\n\n\treturn circonus, initError\n}\n\ntype CirconusProvider struct {\n\tmetrics *cgm.CirconusMetrics\n\tprefix  string\n}\n\nfunc (cp *CirconusProvider) metricName(name string) string {\n\treturn fmt.Sprintf(\"%s`%s\", cp.prefix, name)\n}\n\nfunc (cp *CirconusProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\treturn &cgmCounter{\n\t\tp:           cp,\n\t\tname:        cp.metricName(name),\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\nfunc (cp *CirconusProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\treturn &cgmGauge{\n\t\tp:           cp,\n\t\tname:        cp.metricName(name),\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\nfunc (cp *CirconusProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\treturn &cgmTimer{\n\t\tp:           cp,\n\t\tname:        cp.metricName(name),\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\ntype cgmCounter struct {\n\tp           *CirconusProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (c *cgmCounter) With(labelValues ...string) gkm.Counter {\n\tvar name string\n\tswitch c.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(c.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tname = c.p.metricName(name)\n\tcase false:\n\t\tname = Flatten(c.name, labelValues, DotSeparator)\n\t}\n\treturn &cgmCounter{\n\t\tp:           c.p,\n\t\tname:        name,\n\t\trouteMetric: c.routeMetric,\n\t}\n}\n\nfunc (c *cgmCounter) Add(delta float64) {\n\tc.p.metrics.IncrementByValue(c.name, uint64(delta))\n}\n\ntype cgmGauge struct {\n\tp           *CirconusProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (g *cgmGauge) With(labelValues ...string) gkm.Gauge {\n\tvar name string\n\tswitch g.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(g.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tname = g.p.metricName(name)\n\tcase false:\n\t\tname = Flatten(g.name, labelValues, DotSeparator)\n\t}\n\treturn &cgmGauge{\n\t\tp:           g.p,\n\t\tname:        name,\n\t\trouteMetric: g.routeMetric,\n\t}\n}\n\nfunc (g *cgmGauge) Set(value float64) {\n\tg.p.metrics.Gauge(g.name, value)\n}\n\nfunc (g *cgmGauge) Add(delta float64) {\n\tg.p.metrics.AddGauge(g.name, delta)\n}\n\ntype cgmTimer struct {\n\tp           *CirconusProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (t *cgmTimer) With(labelValues ...string) gkm.Histogram {\n\tvar name string\n\tswitch t.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(t.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tname = t.p.metricName(name)\n\tcase false:\n\t\tname = Flatten(t.name, labelValues, DotSeparator)\n\t}\n\treturn &cgmTimer{\n\t\tp:           t.p,\n\t\tname:        name,\n\t\trouteMetric: t.routeMetric,\n\t}\n}\n\nfunc (t *cgmTimer) Observe(value float64) {\n\tt.p.metrics.Timing(t.name, value*float64(time.Second))\n}\n"
  },
  {
    "path": "metrics/provider_circonus_test.go",
    "content": "package metrics\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\nfunc TestAll(t *testing.T) {\n\tstart := time.Now()\n\n\tif os.Getenv(\"CIRCONUS_API_TOKEN\") == \"\" && os.Getenv(\"CIRCONUS_SUBMISSION_URL\") == \"\" {\n\t\tt.Skip(\"skipping test; $CIRCONUS_API_TOKEN or $CIRCONUS_SUBMISSION_URL not set\")\n\t}\n\n\tt.Log(\"Testing cgm functionality -- this *will* create/use a check\")\n\n\tcfg := config.Circonus{\n\t\tSubmissionURL: os.Getenv(\"CIRCONUS_SUBMISSION_URL\"),\n\t\tAPIKey:        os.Getenv(\"CIRCONUS_API_TOKEN\"),\n\t\tAPIApp:        os.Getenv(\"CIRCONUS_API_APP\"),\n\t\tAPIURL:        os.Getenv(\"CIRCONUS_API_URL\"),\n\t\tCheckID:       os.Getenv(\"CIRCONUS_CHECK_ID\"),\n\t\tBrokerID:      os.Getenv(\"CIRCONUS_BROKER_ID\"),\n\t}\n\n\tinterval, err := time.ParseDuration(\"60s\")\n\tif err != nil {\n\t\tt.Fatalf(\"Unable to parse interval %+v\", err)\n\t}\n\n\tcirc, err := NewCirconusProvider(\"test\", cfg, interval)\n\tif err != nil {\n\t\tt.Fatalf(\"Unable to initialize Circonus +%v\", err)\n\t}\n\n\tcounter := circ.NewCounter(\"fooCounter\")\n\tcounter.Add(3)\n\n\ttimer := circ.NewHistogram(\"fooTimer\")\n\ttimer.Observe(time.Since(start).Seconds())\n\n\tcirconus.metrics.Flush()\n}\n"
  },
  {
    "path": "metrics/provider_discard.go",
    "content": "package metrics\n\nimport (\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"github.com/go-kit/kit/metrics/discard\"\n)\n\ntype DiscardProvider struct{}\n\nfunc (dp DiscardProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\treturn discard.NewCounter()\n}\n\nfunc (dp DiscardProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\treturn discard.NewGauge()\n}\n\nfunc (dp DiscardProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\treturn discard.NewHistogram()\n}\n"
  },
  {
    "path": "metrics/provider_dogstatsd.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"github.com/go-kit/kit/metrics/dogstatsd\"\n\t\"github.com/go-kit/log\"\n)\n\ntype DogstatsdProvider struct {\n\tD *dogstatsd.Dogstatsd\n}\n\nfunc (dp *DogstatsdProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\treturn &dogstatsdCounter{dp.D.NewCounter(name, 1)}\n}\n\nfunc (dp *DogstatsdProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\treturn &dogstatsdGauge{dp.D.NewGauge(name)}\n}\n\nfunc (dp *DogstatsdProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\treturn &dogstatsdHistogram{dp.D.NewHistogram(name, 1)}\n}\n\nfunc NewDogstatsdProvider(prefix, addr string, interval time.Duration) (*DogstatsdProvider, error) {\n\td := &DogstatsdProvider{\n\t\tD: dogstatsd.New(prefix, log.NewNopLogger()),\n\t}\n\t_, err := net.ResolveUDPAddr(\"udp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error resolving dogstatsd address %s: %w\", addr, err)\n\t}\n\tt := time.NewTicker(interval)\n\tgo func() {\n\t\td.D.SendLoop(context.Background(), t.C, \"udp\", addr)\n\t}()\n\treturn d, nil\n}\n\ntype dogstatsdCounter struct {\n\tgkm.Counter\n}\ntype dogstatsdGauge struct {\n\tgkm.Gauge\n}\ntype dogstatsdHistogram struct {\n\tgkm.Histogram\n}\n\nfunc (dh *dogstatsdHistogram) Observe(value float64) {\n\tdh.Histogram.Observe(value * 1000.0)\n}\n\nfunc (dh *dogstatsdCounter) With(labelValues ...string) gkm.Counter {\n\treturn dh.Counter.With(correctReservedTagKeys(labelValues)...)\n}\n\nfunc (dh *dogstatsdGauge) With(labelValues ...string) gkm.Gauge {\n\treturn dh.Gauge.With(correctReservedTagKeys(labelValues)...)\n}\n\nfunc (dh *dogstatsdHistogram) With(labelValues ...string) gkm.Histogram {\n\treturn dh.Histogram.With(correctReservedTagKeys(labelValues)...)\n}\n\nfunc correctReservedTagKeys(labelValues []string) []string {\n\tvar rval []string\n\tfor i, v := range labelValues {\n\t\tif i%2 == 0 {\n\t\t\trval = append(rval, correctReservedTagKey(v))\n\t\t} else {\n\t\t\trval = append(rval, v)\n\t\t}\n\t}\n\treturn rval\n}\n\nfunc correctReservedTagKey(label string) string {\n\tswitch label {\n\tcase \"host\":\n\t\treturn \"fabio-host\"\n\tcase \"device\":\n\t\treturn \"fabio-device\"\n\tcase \"source\":\n\t\treturn \"fabio-source\"\n\tcase \"service\":\n\t\treturn \"fabio-service\"\n\tcase \"env\":\n\t\treturn \"fabio-env\"\n\tcase \"version\":\n\t\treturn \"fabio-version\"\n\tdefault:\n\t\treturn label\n\t}\n}\n"
  },
  {
    "path": "metrics/provider_dogstatsd_test.go",
    "content": "package metrics\n\nimport (\n\t\"bytes\"\n\t\"github.com/go-kit/kit/metrics/dogstatsd\"\n\t\"github.com/go-kit/log\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestDogstatsdProvider(t *testing.T) {\n\tprefix := \"test-\"\n\td := dogstatsd.New(prefix, log.NewNopLogger())\n\tprovider := &DogstatsdProvider{D: d}\n\tfor _, tst := range []struct {\n\t\tname            string\n\t\tlabels          []string\n\t\tvalues          []string\n\t\texpected_values []string\n\t\tprefix          string\n\t\tcountval        float64\n\t\tgaugeval        float64\n\t\thistoval        float64\n\t}{\n\t\t{\n\t\t\tname:            \"simpleTest\",\n\t\t\tlabels:          []string{\"service\", \"host\", \"path\", \"target\", \"other\"},\n\t\t\tvalues:          []string{\"service\", \"foo\", \"host\", \"bar\", \"path\", \"/asdf\", \"target\", \"http://jkl.org:1234\", \"other\", \"trailer\"},\n\t\t\texpected_values: []string{\"fabio-service\", \"foo\", \"fabio-host\", \"bar\", \"path\", \"/asdf\", \"target\", \"http://jkl.org:1234\", \"other\", \"trailer\"},\n\t\t\tprefix:          \"tst\",\n\t\t\tcountval:        20,\n\t\t\tgaugeval:        30,\n\t\t\thistoval:        (time.Microsecond * 50).Seconds(),\n\t\t},\n\t} {\n\t\tt.Run(tst.name, func(t *testing.T) {\n\t\t\tcounter := provider.NewCounter(tst.prefix+\".counter\", tst.labels...)\n\t\t\tgauge := provider.NewGauge(tst.prefix+\".gauge\", tst.labels...)\n\t\t\thisto := provider.NewHistogram(tst.prefix+\".histogram\", tst.labels...)\n\t\t\tif len(tst.labels) > 0 {\n\t\t\t\tcounter = counter.With(tst.values...)\n\t\t\t\tgauge = gauge.With(tst.values...)\n\t\t\t\thisto = histo.With(tst.values...)\n\t\t\t}\n\t\t\tcounter.Add(tst.countval)\n\t\t\tgauge.Set(tst.gaugeval)\n\t\t\thisto.Observe(tst.histoval)\n\t\t\tvar buff bytes.Buffer\n\t\t\t_, _ = provider.D.WriteTo(&buff)\n\t\t\tm := parseStatsdMetrics(&buff, prefix)\n\t\t\tfor _, v := range []struct {\n\t\t\t\tn string\n\t\t\t\tv float64\n\t\t\t}{\n\t\t\t\t{tst.prefix + \".counter\", tst.countval},\n\t\t\t\t{tst.prefix + \".gauge\", tst.gaugeval},\n\t\t\t\t{tst.prefix + \".histogram\", tst.histoval},\n\t\t\t} {\n\t\t\t\tif se, ok := m[v.n]; ok {\n\t\t\t\t\tif se.value != v.v {\n\t\t\t\t\t\tt.Errorf(\"%s failed: expected: %.02f, got %02f\", v.n, v.v, se.value)\n\t\t\t\t\t}\n\t\t\t\t\tif len(tst.expected_values) > 0 && !reflect.DeepEqual(se.tags, tst.expected_values) {\n\t\t\t\t\t\tt.Errorf(\"tags did not survive round trip parsing\")\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"%s not found\", v.n)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "metrics/provider_flat.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n)\n\ntype flatProvider struct {\n\tprefix string\n}\n\nfunc (p *flatProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\treturn &flatCounter{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)}\n}\n\nfunc (p *flatProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\treturn &flatGauge{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)}\n}\n\nfunc (p *flatProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\treturn &flatHistogram{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)}\n}\n\ntype flatCounter struct {\n\tName string\n\tv    uint64\n}\n\nfunc (c *flatCounter) With(labelValues ...string) gkm.Counter {\n\treturn c\n}\n\nfunc (c *flatCounter) Add(v float64) {\n\tuv := atomic.AddUint64(&c.v, uint64(v))\n\tfmt.Printf(\"%s:%d|c\\n\", c.Name, uv)\n}\n\ntype flatGauge struct {\n\tName string\n\t// Stolen from prometheus client gauge\n\tvalBits uint64\n}\n\nfunc (g *flatGauge) Set(n float64) {\n\tatomic.StoreUint64(&g.valBits, math.Float64bits(n))\n\tfmt.Printf(\"%s:%d|g\\n\", g.Name, int(n))\n}\n\nfunc (g *flatGauge) Add(delta float64) {\n\tvar oldBits uint64\n\tvar newBits uint64\n\tfor {\n\t\toldBits = atomic.LoadUint64(&g.valBits)\n\t\tnewBits = math.Float64bits(math.Float64frombits(oldBits) + delta)\n\t\tif atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {\n\t\t\tbreak\n\t\t}\n\t}\n\tfmt.Printf(\"%s:%d|g\\n\", g.Name, int(math.Float64frombits(newBits)))\n}\n\nfunc (g *flatGauge) With(labelValues ...string) gkm.Gauge {\n\treturn g\n}\n\ntype flatHistogram struct {\n\tName string\n}\n\nfunc (h *flatHistogram) Observe(t float64) {\n\tfmt.Printf(\":%s:%d|ms\\n\", h.Name, int64(math.Round(t*1000.0)))\n}\nfunc (h *flatHistogram) With(labels ...string) gkm.Histogram {\n\treturn h\n}\n"
  },
  {
    "path": "metrics/provider_graphite.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"github.com/go-kit/kit/metrics/graphite\"\n\t\"github.com/go-kit/log\"\n\t\"net\"\n\t\"time\"\n)\n\ntype GraphiteProvider struct {\n\tG       *graphite.Graphite\n\tbuckets int\n}\n\nfunc (g *GraphiteProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\tif len(labels) == 0 {\n\t\treturn g.G.NewCounter(name)\n\t}\n\treturn &graphiteCounter{\n\t\tp:           g,\n\t\tname:        name,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\nfunc (g *GraphiteProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\tif len(labels) == 0 {\n\t\treturn g.G.NewGauge(name)\n\t}\n\treturn &graphiteGauge{\n\t\tp:           g,\n\t\tname:        name,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\nfunc (g *GraphiteProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\tvar histogram gkm.Histogram\n\tif len(labels) == 0 {\n\t\thistogram = g.G.NewHistogram(name, g.buckets)\n\t}\n\treturn &graphiteHistogram{\n\t\tHistogram:   histogram,\n\t\tp:           g,\n\t\tname:        name,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\nfunc NewGraphiteProvider(prefix, addr string, buckets int, interval time.Duration) (*GraphiteProvider, error) {\n\tg := &GraphiteProvider{\n\t\tG:       graphite.New(prefix, log.NewNopLogger()),\n\t\tbuckets: buckets,\n\t}\n\t_, err := net.ResolveTCPAddr(\"tcp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error resolving graphite address %s: %w\", addr, err)\n\t}\n\tt := time.NewTicker(interval)\n\tgo func() {\n\t\tg.G.SendLoop(context.Background(), t.C, \"tcp\", addr)\n\t}()\n\treturn g, nil\n}\n\ntype graphiteCounter struct {\n\tgkm.Counter\n\tp           *GraphiteProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (c *graphiteCounter) With(labelValues ...string) gkm.Counter {\n\tvar name string\n\tswitch c.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(c.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(c.name, labelValues, DotSeparator)\n\t}\n\treturn &graphiteCounter{\n\t\tCounter:     c.p.G.NewCounter(name),\n\t\tname:        name,\n\t\tp:           c.p,\n\t\trouteMetric: c.routeMetric,\n\t}\n}\n\ntype graphiteGauge struct {\n\tgkm.Gauge\n\tp           *GraphiteProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (g *graphiteGauge) With(labelValues ...string) gkm.Gauge {\n\tvar name string\n\tswitch g.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(g.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(g.name, labelValues, DotSeparator)\n\t}\n\treturn &graphiteGauge{\n\t\tGauge:       g.p.G.NewGauge(name),\n\t\tname:        name,\n\t\tp:           g.p,\n\t\trouteMetric: g.routeMetric,\n\t}\n}\n\ntype graphiteHistogram struct {\n\tgkm.Histogram\n\tp           *GraphiteProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (h *graphiteHistogram) With(labelValues ...string) gkm.Histogram {\n\tvar name string\n\tswitch h.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(h.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(h.name, labelValues, DotSeparator)\n\t}\n\treturn &graphiteHistogram{\n\t\tHistogram:   h.p.G.NewHistogram(name, h.p.buckets),\n\t\tname:        name,\n\t\tp:           h.p,\n\t\trouteMetric: h.routeMetric,\n\t}\n}\n\nfunc (h *graphiteHistogram) Observe(value float64) {\n\th.Histogram.Observe(value * 1000.0)\n}\n"
  },
  {
    "path": "metrics/provider_label.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"math\"\n\t\"strings\"\n\t\"sync/atomic\"\n)\n\ntype labelProvider struct {\n\tprefix string\n}\n\nfunc (p *labelProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\treturn &labelCounter{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels}\n}\n\nfunc (p *labelProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\treturn &labelGauge{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels}\n}\n\nfunc (p *labelProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\treturn &labelHistogram{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels}\n}\n\ntype labelCounter struct {\n\tName   string\n\tLabels []string\n\tValues []string\n\tv      int64\n}\n\nfunc (c *labelCounter) With(labelValues ...string) gkm.Counter {\n\tcc := &labelCounter{\n\t\tName:   c.Name,\n\t\tLabels: c.Labels,\n\t\tValues: make([]string, len(labelValues)),\n\t\tv:      c.v,\n\t}\n\tcopy(cc.Values, labelValues)\n\treturn cc\n}\n\nfunc (c *labelCounter) Inc() {\n\tv := atomic.AddInt64(&c.v, 1)\n\tfmt.Printf(\"%s:%d|c%s\\n\", c.Name, v, Labels(c.Labels, c.Values, \"|#\", \":\", \",\"))\n}\n\nfunc (c *labelCounter) Add(delta float64) {\n\tv := atomic.AddInt64(&c.v, int64(delta))\n\tfmt.Printf(\"%s:%d|c%s\\n\", c.Name, v, Labels(c.Labels, c.Values, \"|#\", \":\", \",\"))\n}\n\ntype labelGauge struct {\n\tName    string\n\tLabels  []string\n\tValues  []string\n\tvalBits uint64\n}\n\nfunc (g *labelGauge) With(labelValues ...string) gkm.Gauge {\n\tgc := &labelGauge{\n\t\tName:   g.Name,\n\t\tLabels: g.Labels,\n\t\tValues: make([]string, len(labelValues)),\n\t}\n\tcopy(gc.Values, labelValues)\n\treturn gc\n}\n\nfunc (g *labelGauge) Set(n float64) {\n\tatomic.StoreUint64(&g.valBits, math.Float64bits(n))\n\tfmt.Printf(\"%s:%d|g%s\\n\", g.Name, int(n), Labels(g.Labels, g.Values, \"|#\", \":\", \",\"))\n}\n\nfunc (g *labelGauge) Add(delta float64) {\n\tvar oldBits uint64\n\tvar newBits uint64\n\tfor {\n\t\toldBits = atomic.LoadUint64(&g.valBits)\n\t\tnewBits = math.Float64bits(math.Float64frombits(oldBits) + delta)\n\t\tif atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {\n\t\t\tbreak\n\t\t}\n\t}\n\tfmt.Printf(\"%s:%d|g%s\\n\", g.Name, int(delta), Labels(g.Labels, g.Values, \"|#\", \":\", \",\"))\n}\n\ntype labelHistogram struct {\n\tName   string\n\tLabels []string\n\tValues []string\n}\n\nfunc (h *labelHistogram) With(labels ...string) gkm.Histogram {\n\th2 := &labelHistogram{}\n\t*h2 = *h\n\th2.Values = make([]string, len(labels))\n\tcopy(h2.Values, labels)\n\treturn h2\n}\n\nfunc (h *labelHistogram) Observe(t float64) {\n\tfmt.Printf(\"%s:%d|ms%s\\n\", h.Name, int64(math.Round(t*1000.0)), Labels(h.Labels, h.Values, \"|#\", \":\", \",\"))\n}\n"
  },
  {
    "path": "metrics/provider_multi.go",
    "content": "package metrics\n\nimport gkm \"github.com/go-kit/kit/metrics\"\n\n// MultiProvider wraps zero or more providers.\ntype MultiProvider struct {\n\tp []Provider\n}\n\nfunc NewMultiProvider(p []Provider) *MultiProvider {\n\treturn &MultiProvider{p}\n}\n\n// NewCounter creates a MultiCounter with counter objects for all registered\n// providers.\nfunc (mp *MultiProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\tvar c []gkm.Counter\n\tfor _, p := range mp.p {\n\t\tc = append(c, p.NewCounter(name, labels...))\n\t}\n\treturn &MultiCounter{c}\n}\n\n// NewGauge creates a MultiGauge with gauge objects for all registered\n// providers.\nfunc (mp *MultiProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\tvar v []gkm.Gauge\n\tfor _, p := range mp.p {\n\t\tv = append(v, p.NewGauge(name, labels...))\n\t}\n\treturn &MultiGauge{v}\n}\n\nfunc (mp *MultiProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\tvar h []gkm.Histogram\n\tfor _, p := range mp.p {\n\t\th = append(h, p.NewHistogram(name, labels...))\n\t}\n\treturn &MultiHistogram{h: h}\n}\n\n// MultiCounter wraps zero or more counters.\ntype MultiCounter struct {\n\tc []gkm.Counter\n}\n\nfunc (mc *MultiCounter) Add(v float64) {\n\tfor _, c := range mc.c {\n\t\tc.Add(v)\n\t}\n}\n\nfunc (mc *MultiCounter) With(labels ...string) gkm.Counter {\n\tcc := make([]gkm.Counter, len(mc.c))\n\tfor i := range mc.c {\n\t\tcc[i] = mc.c[i].With(labels...)\n\t}\n\treturn &MultiCounter{c: cc}\n}\n\n// DeleteLabelValues deletes the metric with the given label values from all underlying counters.\nfunc (mc *MultiCounter) DeleteLabelValues(labelValues ...string) bool {\n\tdeleted := false\n\tfor _, c := range mc.c {\n\t\tif dc, ok := c.(DeletableCounter); ok {\n\t\t\tif dc.DeleteLabelValues(labelValues...) {\n\t\t\t\tdeleted = true\n\t\t\t}\n\t\t}\n\t}\n\treturn deleted\n}\n\n// MultiGauge wraps zero or more gauges.\ntype MultiGauge struct {\n\tv []gkm.Gauge\n}\n\nfunc (m *MultiGauge) Set(n float64) {\n\tfor _, v := range m.v {\n\t\tv.Set(n)\n\t}\n}\n\nfunc (m *MultiGauge) With(labels ...string) gkm.Gauge {\n\tvc := make([]gkm.Gauge, len(m.v))\n\tfor i := range m.v {\n\t\tvc[i] = m.v[i].With(labels...)\n\t}\n\treturn &MultiGauge{v: vc}\n}\n\nfunc (m *MultiGauge) Add(val float64) {\n\tfor _, v := range m.v {\n\t\tv.Add(val)\n\t}\n}\n\n// DeleteLabelValues deletes the metric with the given label values from all underlying gauges.\nfunc (m *MultiGauge) DeleteLabelValues(labelValues ...string) bool {\n\tdeleted := false\n\tfor _, g := range m.v {\n\t\tif dg, ok := g.(DeletableGauge); ok {\n\t\t\tif dg.DeleteLabelValues(labelValues...) {\n\t\t\t\tdeleted = true\n\t\t\t}\n\t\t}\n\t}\n\treturn deleted\n}\n\ntype MultiHistogram struct {\n\th []gkm.Histogram\n}\n\nfunc (m *MultiHistogram) With(labelValues ...string) gkm.Histogram {\n\thc := make([]gkm.Histogram, len(m.h))\n\tfor i := range m.h {\n\t\thc[i] = m.h[i].With(labelValues...)\n\t}\n\treturn &MultiHistogram{h: hc}\n}\n\nfunc (m *MultiHistogram) Observe(value float64) {\n\tfor _, v := range m.h {\n\t\tv.Observe(value)\n\t}\n}\n\n// DeleteLabelValues deletes the metric with the given label values from all underlying histograms.\nfunc (m *MultiHistogram) DeleteLabelValues(labelValues ...string) bool {\n\tdeleted := false\n\tfor _, h := range m.h {\n\t\tif dh, ok := h.(DeletableHistogram); ok {\n\t\t\tif dh.DeleteLabelValues(labelValues...) {\n\t\t\t\tdeleted = true\n\t\t\t}\n\t\t}\n\t}\n\treturn deleted\n}\n"
  },
  {
    "path": "metrics/provider_prometheus.go",
    "content": "package metrics\n\nimport (\n\tgkm \"github.com/go-kit/kit/metrics\"\n\tpromclient \"github.com/prometheus/client_golang/prometheus\"\n)\n\ntype PromProvider struct {\n\tOpts    promclient.Opts\n\tBuckets []float64\n}\n\nfunc NewPromProvider(namespace, subsystem string, buckets []float64) Provider {\n\tnamespace = clean(namespace)\n\tif len(subsystem) > 0 {\n\t\tsubsystem = clean(subsystem)\n\t}\n\treturn &PromProvider{\n\t\tOpts: promclient.Opts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: subsystem,\n\t\t},\n\t\tBuckets: buckets,\n\t}\n}\n\nfunc (p *PromProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\tcopts := promclient.CounterOpts(p.Opts)\n\tcopts.Name = clean(name)\n\tcv := promclient.NewCounterVec(copts, labels)\n\tpromclient.MustRegister(cv)\n\treturn &promCounter{cv: cv}\n}\n\nfunc (p *PromProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\tgopts := promclient.GaugeOpts(p.Opts)\n\tgopts.Name = clean(name)\n\tgv := promclient.NewGaugeVec(gopts, labels)\n\tpromclient.MustRegister(gv)\n\treturn &promGauge{gv: gv}\n}\n\nfunc (p *PromProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\thopts := promclient.HistogramOpts{\n\t\tNamespace:   p.Opts.Namespace,\n\t\tSubsystem:   p.Opts.Subsystem,\n\t\tName:        clean(name),\n\t\tHelp:        p.Opts.Help,\n\t\tConstLabels: p.Opts.ConstLabels,\n\t\tBuckets:     p.Buckets,\n\t}\n\thv := promclient.NewHistogramVec(hopts, labels)\n\tpromclient.MustRegister(hv)\n\treturn &promHistogram{hv: hv}\n}\n\n// makeLabels converts a slice of alternating key-value pairs into prometheus.Labels.\n// e.g., [\"service\", \"foo\", \"host\", \"bar\"] -> {\"service\": \"foo\", \"host\": \"bar\"}\nfunc makeLabels(lvs []string) promclient.Labels {\n\tlabels := promclient.Labels{}\n\tfor i := 0; i < len(lvs); i += 2 {\n\t\tlabels[lvs[i]] = lvs[i+1]\n\t}\n\treturn labels\n}\n\n// promCounter wraps a Prometheus CounterVec and supports deletion of label values.\ntype promCounter struct {\n\tcv  *promclient.CounterVec\n\tlvs []string // alternating key-value pairs\n}\n\nfunc (c *promCounter) Add(delta float64) {\n\tc.cv.With(makeLabels(c.lvs)).Add(delta)\n}\n\nfunc (c *promCounter) With(labelValues ...string) gkm.Counter {\n\treturn &promCounter{\n\t\tcv:  c.cv,\n\t\tlvs: append(append([]string{}, c.lvs...), labelValues...),\n\t}\n}\n\n// DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs).\nfunc (c *promCounter) DeleteLabelValues(labelValues ...string) bool {\n\treturn c.cv.DeleteLabelValues(labelValues...)\n}\n\n// promGauge wraps a Prometheus GaugeVec and supports deletion of label values.\ntype promGauge struct {\n\tgv  *promclient.GaugeVec\n\tlvs []string // alternating key-value pairs\n}\n\nfunc (g *promGauge) Set(value float64) {\n\tg.gv.With(makeLabels(g.lvs)).Set(value)\n}\n\nfunc (g *promGauge) Add(delta float64) {\n\tg.gv.With(makeLabels(g.lvs)).Add(delta)\n}\n\nfunc (g *promGauge) With(labelValues ...string) gkm.Gauge {\n\treturn &promGauge{\n\t\tgv:  g.gv,\n\t\tlvs: append(append([]string{}, g.lvs...), labelValues...),\n\t}\n}\n\n// DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs).\nfunc (g *promGauge) DeleteLabelValues(labelValues ...string) bool {\n\treturn g.gv.DeleteLabelValues(labelValues...)\n}\n\n// promHistogram wraps a Prometheus HistogramVec and supports deletion of label values.\ntype promHistogram struct {\n\thv  *promclient.HistogramVec\n\tlvs []string // alternating key-value pairs\n}\n\nfunc (h *promHistogram) Observe(value float64) {\n\th.hv.With(makeLabels(h.lvs)).Observe(value)\n}\n\nfunc (h *promHistogram) With(labelValues ...string) gkm.Histogram {\n\treturn &promHistogram{\n\t\thv:  h.hv,\n\t\tlvs: append(append([]string{}, h.lvs...), labelValues...),\n\t}\n}\n\n// DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs).\nfunc (h *promHistogram) DeleteLabelValues(labelValues ...string) bool {\n\treturn h.hv.DeleteLabelValues(labelValues...)\n}\n"
  },
  {
    "path": "metrics/provider_prometheus_test.go",
    "content": "package metrics\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n)\n\nfunc TestPromProviderDeleteLabelValues(t *testing.T) {\n\t// Create a new registry to avoid conflicts with other tests\n\treg := prometheus.NewRegistry()\n\n\t// Create histogram directly (not via provider to control registration)\n\thv := prometheus.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"test\",\n\t\tName:      \"route\",\n\t\tHelp:      \"test histogram\",\n\t\tBuckets:   prometheus.DefBuckets,\n\t}, []string{\"service\", \"host\", \"path\", \"target\"})\n\treg.MustRegister(hv)\n\n\tph := &promHistogram{hv: hv}\n\n\t// Create metrics for two targets\n\th1 := ph.With(\"service\", \"svc1\", \"host\", \"host1\", \"path\", \"/path1\", \"target\", \"http://target1/\")\n\th2 := ph.With(\"service\", \"svc2\", \"host\", \"host2\", \"path\", \"/path2\", \"target\", \"http://target2/\")\n\n\t// Observe some values\n\th1.Observe(0.1)\n\th1.Observe(0.2)\n\th2.Observe(0.3)\n\n\t// Verify both metrics exist\n\tcount := testutil.CollectAndCount(hv)\n\tt.Logf(\"Metric count before delete: %d\", count)\n\tif count == 0 {\n\t\tt.Fatal(\"Expected metrics to be registered\")\n\t}\n\n\t// Gather metrics to check labels\n\tmetrics, _ := reg.Gather()\n\tt.Logf(\"Metrics before delete:\")\n\tfor _, m := range metrics {\n\t\tfor _, metric := range m.GetMetric() {\n\t\t\tvar labels []string\n\t\t\tfor _, l := range metric.GetLabel() {\n\t\t\t\tlabels = append(labels, l.GetName()+\"=\"+l.GetValue())\n\t\t\t}\n\t\t\tt.Logf(\"  %s{%s}\", m.GetName(), strings.Join(labels, \", \"))\n\t\t}\n\t}\n\n\t// Delete the first target's metrics (note: values only, not key-value pairs)\n\tdeleted := ph.DeleteLabelValues(\"svc1\", \"host1\", \"/path1\", \"http://target1/\")\n\tt.Logf(\"DeleteLabelValues returned: %v\", deleted)\n\n\t// Gather metrics after delete\n\tmetrics, _ = reg.Gather()\n\tt.Logf(\"Metrics after delete:\")\n\tfoundSvc1 := false\n\tfoundSvc2 := false\n\tfor _, m := range metrics {\n\t\tfor _, metric := range m.GetMetric() {\n\t\t\tvar labels []string\n\t\t\tfor _, l := range metric.GetLabel() {\n\t\t\t\tlabels = append(labels, l.GetName()+\"=\"+l.GetValue())\n\t\t\t\tif l.GetName() == \"service\" && l.GetValue() == \"svc1\" {\n\t\t\t\t\tfoundSvc1 = true\n\t\t\t\t}\n\t\t\t\tif l.GetName() == \"service\" && l.GetValue() == \"svc2\" {\n\t\t\t\t\tfoundSvc2 = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.Logf(\"  %s{%s}\", m.GetName(), strings.Join(labels, \", \"))\n\t\t}\n\t}\n\n\tif foundSvc1 {\n\t\tt.Error(\"svc1 metrics should have been deleted but were still found\")\n\t}\n\tif !foundSvc2 {\n\t\tt.Error(\"svc2 metrics should still exist but were not found\")\n\t}\n}\n\nfunc TestPromHistogramWithLabelValues(t *testing.T) {\n\t// Test that With() correctly accumulates label values\n\thv := prometheus.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"test2\",\n\t\tName:      \"route2\",\n\t\tHelp:      \"test histogram\",\n\t\tBuckets:   prometheus.DefBuckets,\n\t}, []string{\"service\", \"host\", \"path\", \"target\"})\n\n\tph := &promHistogram{hv: hv}\n\n\t// Add labels in stages like the real code does\n\th := ph.With(\"service\", \"mysvc\", \"host\", \"myhost\", \"path\", \"/mypath\", \"target\", \"http://mytarget/\")\n\n\t// Check internal state\n\tph2 := h.(*promHistogram)\n\tt.Logf(\"lvs after With: %v\", ph2.lvs)\n\tt.Logf(\"lvs length: %d\", len(ph2.lvs))\n\n\texpectedLvs := []string{\"service\", \"mysvc\", \"host\", \"myhost\", \"path\", \"/mypath\", \"target\", \"http://mytarget/\"}\n\tif len(ph2.lvs) != len(expectedLvs) {\n\t\tt.Errorf(\"Expected lvs length %d, got %d\", len(expectedLvs), len(ph2.lvs))\n\t}\n\n\t// Try to observe - this should not panic\n\th.Observe(0.5)\n\tt.Log(\"Observe succeeded\")\n}\n"
  },
  {
    "path": "metrics/provider_statsd.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"github.com/go-kit/kit/metrics/statsd\"\n\t\"github.com/go-kit/log\"\n)\n\ntype StatsdProvider struct {\n\tS *statsd.Statsd\n}\n\nfunc NewStatsdProvider(prefix, addr string, interval time.Duration) (*StatsdProvider, error) {\n\tp := &StatsdProvider{\n\t\tS: statsd.New(prefix, log.NewNopLogger()),\n\t}\n\t_, err := net.ResolveUDPAddr(\"udp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error resolving statsd address %s: %w\", addr, err)\n\t}\n\tt := time.NewTicker(interval)\n\tgo func() {\n\t\tp.S.SendLoop(context.Background(), t.C, \"udp\", addr)\n\t}()\n\n\treturn p, nil\n}\n\n// NewCounter - This assumes if there are labels, there will be a With() call\nfunc (p *StatsdProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\tif len(labels) == 0 {\n\t\treturn p.S.NewCounter(name, 1)\n\t}\n\treturn &statsdCounter{\n\t\tname:        name,\n\t\tp:           p,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\n// NewGauge - this assumes if there are labels, there will be a With() call.\nfunc (p *StatsdProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\tif len(labels) == 0 {\n\t\treturn p.S.NewGauge(name)\n\t}\n\treturn &statsdGauge{\n\t\tname:        name,\n\t\tp:           p,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\n// NewHistogram - this assumes if there are labels, there will be a With() call.\nfunc (p *StatsdProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\tvar histogram gkm.Histogram\n\tif len(labels) == 0 {\n\t\thistogram = p.S.NewTiming(name, 1)\n\t}\n\treturn &statsdHistogram{\n\t\tHistogram:   histogram,\n\t\tname:        name,\n\t\tp:           p,\n\t\trouteMetric: isRouteMetric(name),\n\t}\n}\n\ntype statsdCounter struct {\n\tgkm.Counter\n\tp           *StatsdProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (c *statsdCounter) With(labelValues ...string) gkm.Counter {\n\tvar name string\n\tswitch c.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(c.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(c.name, labelValues, DotSeparator)\n\t}\n\treturn &statsdCounter{\n\t\tCounter:     c.p.S.NewCounter(name, 1),\n\t\tname:        name,\n\t\tp:           c.p,\n\t\trouteMetric: c.routeMetric,\n\t}\n}\n\ntype statsdGauge struct {\n\tgkm.Gauge\n\tp           *StatsdProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (g *statsdGauge) With(labelValues ...string) gkm.Gauge {\n\tvar name string\n\tswitch g.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(g.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(g.name, labelValues, DotSeparator)\n\t}\n\treturn &statsdGauge{\n\t\tGauge:       g.p.S.NewGauge(name),\n\t\tname:        name,\n\t\tp:           g.p,\n\t\trouteMetric: g.routeMetric,\n\t}\n}\n\ntype statsdHistogram struct {\n\tgkm.Histogram\n\tp           *StatsdProvider\n\tname        string\n\trouteMetric bool\n}\n\nfunc (h *statsdHistogram) With(labelValues ...string) gkm.Histogram {\n\tvar name string\n\tswitch h.routeMetric {\n\tcase true:\n\t\tvar err error\n\t\tname, err = TargetNameWith(h.name, labelValues)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\tcase false:\n\t\tname = Flatten(h.name, labelValues, DotSeparator)\n\t}\n\treturn &statsdHistogram{\n\t\tHistogram:   h.p.S.NewTiming(name, 1),\n\t\tname:        name,\n\t\tp:           h.p,\n\t\trouteMetric: h.routeMetric,\n\t}\n}\n\nfunc (h *statsdHistogram) Observe(value float64) {\n\th.Histogram.Observe(value * 1000.0)\n}\n"
  },
  {
    "path": "metrics/provider_statsd_test.go",
    "content": "package metrics\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"github.com/go-kit/kit/metrics/statsd\"\n\t\"github.com/go-kit/log\"\n\t\"io\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestStatsdProvider(t *testing.T) {\n\tprefix := \"test-\"\n\ts := statsd.New(prefix, log.NewNopLogger())\n\tprovider := &StatsdProvider{S: s}\n\tfor _, tst := range []struct {\n\t\tname         string\n\t\tmetricname   string\n\t\texpectedname string\n\t\tlabels       []string\n\t\tvalues       []string\n\t\tcountval     float64\n\t\tgaugeval     float64\n\t\thistoval     float64\n\t}{\n\t\t{\n\t\t\tname:         \"simpleMetrics\",\n\t\t\tmetricname:   \"simple\",\n\t\t\texpectedname: \"simple\",\n\t\t\tcountval:     1,\n\t\t\tgaugeval:     2,\n\t\t\thistoval:     (time.Millisecond * 5).Seconds(),\n\t\t},\n\t\t{\n\t\t\tname:         \"routeMetrics\",\n\t\t\tmetricname:   \"route\",\n\t\t\tlabels:       []string{\"service\", \"host\", \"path\", \"target\"},\n\t\t\tvalues:       []string{\"service\", \"foo\", \"host\", \"bar\", \"path\", \"/asdf\", \"target\", \"http://jkl.org:1234\"},\n\t\t\texpectedname: \"foo.bar./asdf.jkl_org_1234\",\n\t\t\tcountval:     20,\n\t\t\tgaugeval:     30,\n\t\t\thistoval:     (time.Millisecond * 50).Seconds(),\n\t\t},\n\t\t{\n\t\t\tname:         \"codeMetrics\",\n\t\t\tmetricname:   \"status\",\n\t\t\tlabels:       []string{\"code\"},\n\t\t\tvalues:       []string{\"code\", \"200\"},\n\t\t\texpectedname: \"status.{type}.code.200\",\n\t\t\tcountval:     60,\n\t\t\tgaugeval:     70,\n\t\t\thistoval:     (time.Millisecond * 80).Seconds(),\n\t\t},\n\t} {\n\t\tt.Run(tst.name, func(t *testing.T) {\n\t\t\tcname := tst.metricname + \".count\"\n\t\t\tgname := tst.metricname + \".gauge\"\n\t\t\thname := tst.metricname + \".histo\"\n\t\t\tcounter := provider.NewCounter(cname, tst.labels...)\n\t\t\tgauge := provider.NewGauge(gname, tst.labels...)\n\t\t\thisto := provider.NewHistogram(hname, tst.labels...)\n\t\t\tif len(tst.labels) > 0 {\n\t\t\t\tcounter = counter.With(tst.values...)\n\t\t\t\tgauge = gauge.With(tst.values...)\n\t\t\t\thisto = histo.With(tst.values...)\n\t\t\t}\n\t\t\tcounter.Add(tst.countval)\n\t\t\tgauge.Set(tst.gaugeval)\n\t\t\thisto.Observe(tst.histoval)\n\t\t\tvar buff bytes.Buffer\n\t\t\t_, _ = provider.S.WriteTo(&buff)\n\t\t\tm := parseStatsdMetrics(&buff, prefix)\n\t\t\t// t.Logf(\"parsed metrics: %#v\", m)\n\n\t\t\tfor _, v := range []struct {\n\t\t\t\tn string\n\t\t\t\tv float64\n\t\t\t}{\n\t\t\t\t{\"count\", tst.countval},\n\t\t\t\t{\"gauge\", tst.gaugeval},\n\t\t\t\t{\"histo\", tst.histoval * 1000.0},\n\t\t\t} {\n\t\t\t\tvar name string\n\t\t\t\t// have to do this little dance because route metrics\n\t\t\t\t// follow a special rule\n\t\t\t\tif strings.Contains(tst.expectedname, \"{type}\") {\n\t\t\t\t\tname = strings.ReplaceAll(tst.expectedname, \"{type}\", v.n)\n\t\t\t\t} else {\n\t\t\t\t\tname = tst.expectedname + \".\" + v.n\n\t\t\t\t}\n\t\t\t\tif se, ok := m[name]; ok {\n\t\t\t\t\tif se.value != v.v {\n\t\t\t\t\t\tt.Errorf(\"%s failed: expected: %.02f, got: %02f\", name, v.v, se.value)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"%s not found\", v.n)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n}\n\ntype statsdEntry struct {\n\tvalue  float64\n\tt      string\n\tsample float64\n\ttags   []string\n}\n\nvar re = regexp.MustCompile(`^([^:]+):([0-9\\.]+)\\|(ms|c|g|h)(?:\\|@([0-9\\.]+))?(?:\\|#(.*))?$`)\n\nfunc parseStatsdMetrics(data io.Reader, prefix string) map[string]statsdEntry {\n\treader := bufio.NewScanner(data)\n\tm := make(map[string]statsdEntry)\n\tfor reader.Scan() {\n\t\tline := reader.Text()\n\t\tmatches := re.FindStringSubmatch(line)\n\t\tif matches == nil {\n\t\t\tpanic(line)\n\t\t}\n\t\tname := strings.TrimPrefix(matches[1], prefix)\n\t\tvalue, err := strconv.ParseFloat(matches[2], 64)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t\tvar sample float64\n\t\tif len(matches[4]) > 0 {\n\t\t\tsample, err = strconv.ParseFloat(matches[4], 64)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err.Error)\n\t\t\t}\n\t\t}\n\t\tvar tags []string\n\t\tif len(matches[5]) > 0 {\n\t\t\tkvs := strings.Split(matches[5], \",\")\n\t\t\tfor _, kv := range kvs {\n\t\t\t\ttags = append(tags, strings.SplitN(kv, \":\", 2)...)\n\t\t\t}\n\t\t}\n\t\tm[name] = statsdEntry{\n\t\t\tvalue:  value,\n\t\t\tt:      matches[3],\n\t\t\tsample: sample,\n\t\t\ttags:   tags,\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "noroute/store.go",
    "content": "package noroute\n\nimport (\n\t\"sync/atomic\"\n)\n\nvar store atomic.Value // string\n\nfunc init() {\n\tstore.Store(\"\")\n}\n\n// GetHTML returns the HTML for not found routes.\nfunc GetHTML() string {\n\treturn store.Load().(string)\n}\n\n// SetHTML sets the HTML for not found routes.\nfunc SetHTML(h string) {\n\tstore.Store(h)\n}\n"
  },
  {
    "path": "noroute/store_test.go",
    "content": "package noroute\n\nimport (\n\t\"testing\"\n)\n\nfunc TestStoreSetGet(t *testing.T) {\n\tif got, want := GetHTML(), \"\"; got != want {\n\t\tt.Fatalf(\"got unset noroute html %q want %q\", got, want)\n\t}\n\n\tSetHTML(\"foo\")\n\tif got, want := GetHTML(), \"foo\"; got != want {\n\t\tt.Fatalf(\"got noroute html %q want %q\", got, want)\n\t}\n}\n"
  },
  {
    "path": "proxy/grpc_handler.go",
    "content": "package proxy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/route\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/connectivity\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/stats\"\n\t\"google.golang.org/grpc/status\"\n)\n\ntype gRPCServer struct {\n\tserver *grpc.Server\n}\n\nfunc (s *gRPCServer) Close() error {\n\ts.server.Stop()\n\treturn nil\n}\n\nfunc (s *gRPCServer) Shutdown(ctx context.Context) error {\n\ts.server.GracefulStop()\n\treturn nil\n}\n\nfunc (s *gRPCServer) Serve(lis net.Listener) error {\n\treturn s.server.Serve(lis)\n}\n\nfunc GetGRPCDirector(tlscfg *tls.Config, cfg *config.Config) func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {\n\n\tconnectionPool := newGrpcConnectionPool(tlscfg, cfg)\n\n\treturn func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {\n\t\tmd, ok := metadata.FromIncomingContext(ctx)\n\n\t\tif !ok {\n\t\t\treturn ctx, nil, fmt.Errorf(\"error extracting metadata from request\")\n\t\t}\n\n\t\toutCtx := metadata.NewOutgoingContext(ctx, md.Copy())\n\n\t\ttarget, _ := ctx.Value(targetKey{}).(*route.Target)\n\n\t\tif target == nil {\n\t\t\tlog.Println(\"[WARN] grpc: no route for \", fullMethodName)\n\t\t\treturn outCtx, nil, fmt.Errorf(\"no route found\")\n\t\t}\n\n\t\tconn, err := connectionPool.Get(outCtx, target)\n\n\t\treturn outCtx, conn, err\n\t}\n\n}\n\ntype GrpcProxyInterceptor struct {\n\tConfig       *config.Config\n\tStatsHandler *GrpcStatsHandler\n\tGlobCache    *route.GlobCache\n}\n\ntype targetKey struct{}\n\ntype proxyStream struct {\n\tgrpc.ServerStream\n\tctx context.Context\n}\n\nfunc (p proxyStream) Context() context.Context {\n\treturn p.ctx\n}\n\nfunc makeGRPCTargetKey(t *route.Target) string {\n\treturn t.URL.String()\n}\n\nfunc (g GrpcProxyInterceptor) Stream(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {\n\tctx := stream.Context()\n\n\ttarget, err := g.lookup(ctx, info.FullMethod)\n\n\tif err != nil {\n\t\tlog.Println(\"[ERROR] grpc: error looking up route\", err)\n\t\treturn status.Error(codes.Internal, \"internal error\")\n\t}\n\n\tif target == nil {\n\t\tg.StatsHandler.NoRoute.Add(1)\n\t\tlog.Println(\"[WARN] grpc: no route found for\", info.FullMethod)\n\t\treturn status.Error(codes.NotFound, \"no route found\")\n\t}\n\n\tctx = context.WithValue(ctx, targetKey{}, target)\n\n\tproxyStream := proxyStream{\n\t\tServerStream: stream,\n\t\tctx:          ctx,\n\t}\n\n\tstart := time.Now()\n\n\terr = handler(srv, proxyStream)\n\n\tend := time.Now()\n\tdur := end.Sub(start)\n\n\ttarget.Timer.Observe(dur.Seconds())\n\n\treturn err\n}\n\nfunc (g GrpcProxyInterceptor) lookup(ctx context.Context, fullMethodName string) (*route.Target, error) {\n\tpick := route.Picker[g.Config.Proxy.Strategy]\n\tmatch := route.Matcher[g.Config.Proxy.Matcher]\n\n\tmd, ok := metadata.FromIncomingContext(ctx)\n\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"error extracting metadata from request\")\n\t}\n\n\treqUrl, err := url.ParseRequestURI(fullMethodName)\n\n\tif err != nil {\n\t\tlog.Print(\"[WARN] Error parsing grpc request url \", fullMethodName)\n\t\treturn nil, fmt.Errorf(\"error parsing request url\")\n\t}\n\n\theaders := http.Header{}\n\n\tfor k, v := range md {\n\t\tfor _, h := range v {\n\t\t\theaders.Add(k, h)\n\t\t}\n\t}\n\n\t//grpc client can specify a destination host in metadata\n\tdstHostSpecifiedByGRPCClient := g.getDestinationHostFromMetadata(md)\n\t//todo: better a configuration flag is required to disable/enable this function, and make it disabled by default configuration\n\n\treq := &http.Request{\n\t\tHost:   dstHostSpecifiedByGRPCClient,\n\t\tURL:    reqUrl,\n\t\tHeader: headers,\n\t}\n\n\treturn route.GetTable().Lookup(req, pick, match, g.GlobCache, g.Config.GlobMatchingDisabled), nil\n}\n\n// grpc client can specify a destination host in metadata by key 'dsthost', e.g. dsthost=betatest\n// the backend service(s) tags should be urlprefix-betatest/grpcpackage.servicename proto=grpc\n// the 'betatest' will be parsed as 'host' and '/grpcpackage.servicename' is the 'path',\n// a route record will be setup in route Table, t['betatest']\n// the dstHost is extracted from context's metadata of grpc client, that will trigger t[dstHost] is used.\n// if t[dstHost] not exists, fallback to t[\"\"] is used\n// dstHost will be \"\" as before if not specified by grpc client side.\nfunc (g GrpcProxyInterceptor) getDestinationHostFromMetadata(md metadata.MD) (dstHost string) {\n\tdstHost = \"\"\n\thosts := md[\"dsthost\"]\n\tif len(hosts) == 1 {\n\t\tdstHost = hosts[0]\n\t}\n\treturn\n}\n\ntype GrpcStatsHandler struct {\n\tConnect gkm.Counter\n\tRequest gkm.Histogram\n\tNoRoute gkm.Counter\n\tStatus  gkm.Histogram\n}\n\ntype connCtxKey struct{}\ntype rpcCtxKey struct{}\n\nfunc (h *GrpcStatsHandler) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {\n\treturn context.WithValue(ctx, connCtxKey{}, info)\n}\n\nfunc (h *GrpcStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {\n\treturn context.WithValue(ctx, rpcCtxKey{}, info)\n}\n\nfunc (h *GrpcStatsHandler) HandleRPC(ctx context.Context, rpc stats.RPCStats) {\n\n\trpcStats, _ := rpc.(*stats.End)\n\n\tif rpcStats == nil {\n\t\treturn\n\t}\n\n\tdur := rpcStats.EndTime.Sub(rpcStats.BeginTime)\n\n\th.Request.Observe(dur.Seconds())\n\n\ts, _ := status.FromError(rpcStats.Error)\n\n\th.Status.With(\"code\", s.Code().String()).Observe(dur.Seconds())\n}\n\n// HandleConn processes the Conn stats.\nfunc (h *GrpcStatsHandler) HandleConn(ctx context.Context, conn stats.ConnStats) {\n\tconnBegin, _ := conn.(*stats.ConnBegin)\n\n\tif connBegin != nil {\n\t\th.Connect.Add(1)\n\t}\n}\n\ntype grpcConnectionPool struct {\n\tconnections     map[string]*grpc.ClientConn\n\ttlscfg          *tls.Config\n\tcfg             *config.Config\n\tcleanupInterval time.Duration\n\tlock            sync.RWMutex\n}\n\nfunc newGrpcConnectionPool(tlscfg *tls.Config, cfg *config.Config) *grpcConnectionPool {\n\tcp := &grpcConnectionPool{\n\t\tconnections:     make(map[string]*grpc.ClientConn),\n\t\tlock:            sync.RWMutex{},\n\t\tcleanupInterval: time.Second * 5,\n\t\ttlscfg:          tlscfg,\n\t\tcfg:             cfg,\n\t}\n\n\tgo cp.cleanup()\n\n\treturn cp\n}\n\nfunc (p *grpcConnectionPool) Get(ctx context.Context, target *route.Target) (*grpc.ClientConn, error) {\n\tp.lock.RLock()\n\tconn := p.connections[makeGRPCTargetKey(target)]\n\tp.lock.RUnlock()\n\n\tif conn != nil && conn.GetState() != connectivity.Shutdown {\n\t\treturn conn, nil\n\t}\n\n\treturn p.newConnection(target)\n}\n\nfunc (p *grpcConnectionPool) newConnection(target *route.Target) (*grpc.ClientConn, error) {\n\topts := []grpc.DialOption{\n\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(p.cfg.Proxy.GRPCMaxRxMsgSize)),\n\t}\n\n\tif target.URL.Scheme == \"grpcs\" && p.tlscfg != nil {\n\t\topts = append(opts, grpc.WithTransportCredentials(\n\t\t\tcredentials.NewTLS(&tls.Config{\n\t\t\t\tClientCAs:          p.tlscfg.ClientCAs,\n\t\t\t\tInsecureSkipVerify: target.TLSSkipVerify,\n\t\t\t\t// as per the http/2 spec, the host header isn't required, so if your\n\t\t\t\t// target service doesn't have IP SANs in it's certificate\n\t\t\t\t// then you will need to override the servername\n\t\t\t\tServerName: target.Opts[\"grpcservername\"],\n\t\t\t})))\n\t} else {\n\t\topts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\t}\n\n\tconn, err := grpc.NewClient(target.URL.Host, opts...)\n\n\tif err == nil {\n\t\tp.Set(target, conn)\n\t}\n\n\treturn conn, err\n}\n\nfunc (p *grpcConnectionPool) Set(target *route.Target, conn *grpc.ClientConn) {\n\tp.lock.Lock()\n\tdefer p.lock.Unlock()\n\n\tp.connections[makeGRPCTargetKey(target)] = conn\n}\n\nfunc (p *grpcConnectionPool) cleanup() {\n\tfor {\n\t\tp.lock.Lock()\n\t\ttable := route.GetTable()\n\t\tfor tKey, cs := range p.connections {\n\t\t\tstate := cs.GetState()\n\t\t\tif state == connectivity.Shutdown {\n\t\t\t\tdelete(p.connections, tKey)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !hasTarget(tKey, table) {\n\t\t\t\tlog.Println(\"[DEBUG] grpc: cleaning up connection to\", tKey)\n\t\t\t\tgo func(cs *grpc.ClientConn, state connectivity.State) {\n\t\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), p.cfg.Proxy.GRPCGShutdownTimeout)\n\t\t\t\t\tdefer cancel()\n\t\t\t\t\t// wait for state to change, or timeout, before closing, in case it's still handling traffic.\n\t\t\t\t\tcs.WaitForStateChange(ctx, state)\n\t\t\t\t\tcs.Close()\n\t\t\t\t}(cs, state)\n\t\t\t\tdelete(p.connections, tKey)\n\t\t\t}\n\t\t}\n\t\tp.lock.Unlock()\n\t\ttime.Sleep(p.cleanupInterval)\n\t}\n}\n\nfunc hasTarget(tKey string, table route.Table) bool {\n\tfor _, routes := range table {\n\t\tfor _, r := range routes {\n\t\t\tfor _, t := range r.Targets {\n\t\t\t\tif tKey == makeGRPCTargetKey(t) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "proxy/gzip/content_type_test.go",
    "content": "package gzip\n\nimport \"testing\"\n\n// TestContentTypes tests the content-type regexp that is used as\n// an example in fabio.properties\nfunc TestContentTypes(t *testing.T) {\n\ttests := []string{\n\t\t\"text/foo\",\n\t\t\"text/foo; charset=UTF-8\",\n\t\t\"text/plain\",\n\t\t\"text/plain; charset=UTF-8\",\n\t\t\"application/json\",\n\t\t\"application/json; charset=UTF-8\",\n\t\t\"application/javascript\",\n\t\t\"application/javascript; charset=UTF-8\",\n\t\t\"application/font-woff\",\n\t\t\"application/font-woff; charset=UTF-8\",\n\t\t\"application/xml\",\n\t\t\"application/xml; charset=UTF-8\",\n\t\t\"vendor/vendor.foo+json\",\n\t\t\"vendor/vendor.foo+json; charset=UTF-8\",\n\t\t\"vendor/vendor.foo+xml\",\n\t\t\"vendor/vendor.foo+xml; charset=UTF-8\",\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt, func(t *testing.T) {\n\t\t\tif !contentTypes.MatchString(tt) {\n\t\t\t\tt.Fatalf(\"%q does not match content types regexp\", tt)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proxy/gzip/gzip_handler.go",
    "content": "// Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed\n\n// Package gzip provides an HTTP handler which compresses responses\n// if the client supports this, the response is compressable and\n// not already compressed.\n//\n// Based on https://github.com/smancke/handler/gzip\npackage gzip\n\nimport (\n\t\"bufio\"\n\t\"compress/gzip\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n)\n\nconst (\n\theaderVary            = \"Vary\"\n\theaderAccept          = \"Accept\"\n\theaderAcceptEncoding  = \"Accept-Encoding\"\n\theaderContentEncoding = \"Content-Encoding\"\n\theaderContentType     = \"Content-Type\"\n\theaderContentLength   = \"Content-Length\"\n\tencodingGzip          = \"gzip\"\n)\n\nvar blacklistedAcceptContentTypes = []string{\"text/event-stream\"}\n\nvar gzipWriterPool = sync.Pool{\n\tNew: func() interface{} { return gzip.NewWriter(nil) },\n}\n\n// NewGzipHandler wraps an existing handler to transparently gzip the response\n// body if the client supports it (via the Accept-Encoding header) and the\n// response Content-Type matches the contentTypes expression.\nfunc NewGzipHandler(h http.Handler, contentTypes *regexp.Regexp) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Add(headerVary, headerAcceptEncoding)\n\n\t\tif acceptsGzip(r) {\n\t\t\tgzWriter := NewGzipResponseWriter(w, contentTypes)\n\t\t\tdefer gzWriter.Close()\n\t\t\th.ServeHTTP(gzWriter, r)\n\t\t} else {\n\t\t\th.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\ntype GzipResponseWriter struct {\n\twriter       io.Writer\n\tgzipWriter   *gzip.Writer\n\tcontentTypes *regexp.Regexp\n\thttp.ResponseWriter\n}\n\nfunc NewGzipResponseWriter(w http.ResponseWriter, contentTypes *regexp.Regexp) *GzipResponseWriter {\n\treturn &GzipResponseWriter{ResponseWriter: w, contentTypes: contentTypes}\n}\n\nfunc (grw *GzipResponseWriter) WriteHeader(code int) {\n\tif grw.writer == nil {\n\t\tif isCompressable(grw.Header(), grw.contentTypes) {\n\t\t\tgrw.Header().Del(headerContentLength)\n\t\t\tgrw.Header().Set(headerContentEncoding, encodingGzip)\n\t\t\tgrw.gzipWriter = gzipWriterPool.Get().(*gzip.Writer)\n\t\t\tgrw.gzipWriter.Reset(grw.ResponseWriter)\n\n\t\t\tgrw.writer = grw.gzipWriter\n\t\t} else {\n\t\t\tgrw.writer = grw.ResponseWriter\n\t\t}\n\t}\n\tgrw.ResponseWriter.WriteHeader(code)\n}\n\nfunc (grw *GzipResponseWriter) Write(b []byte) (int, error) {\n\tif grw.writer == nil {\n\t\tif _, ok := grw.Header()[headerContentType]; !ok {\n\t\t\t// Set content-type if not present. Otherwise golang would make application/gzip out of that.\n\t\t\tgrw.Header().Set(headerContentType, http.DetectContentType(b))\n\t\t}\n\t\tgrw.WriteHeader(http.StatusOK)\n\t}\n\treturn grw.writer.Write(b)\n}\n\nfunc (grw *GzipResponseWriter) Close() {\n\tif grw.gzipWriter != nil {\n\t\tgrw.gzipWriter.Close()\n\t\tgzipWriterPool.Put(grw.gzipWriter)\n\t}\n}\n\nfunc (grw *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\tif hj, ok := grw.ResponseWriter.(http.Hijacker); ok {\n\t\treturn hj.Hijack()\n\t}\n\treturn nil, nil, errors.New(\"not a Hijacker\")\n}\n\nfunc isCompressable(header http.Header, contentTypes *regexp.Regexp) bool {\n\t// don't compress if it is already encoded\n\tif header.Get(headerContentEncoding) != \"\" {\n\t\treturn false\n\t}\n\treturn contentTypes.MatchString(header.Get(headerContentType))\n}\n\nfunc acceptsGzip(r *http.Request) bool {\n\taccept := r.Header.Get(headerAccept)\n\tfor _, contentType := range blacklistedAcceptContentTypes {\n\t\tif strings.Contains(accept, contentType) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip)\n}\n"
  },
  {
    "path": "proxy/gzip/gzip_handler_test.go",
    "content": "// Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed\npackage gzip\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/assert\"\n)\n\nvar contentTypes = regexp.MustCompile(`^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$`)\n\nfunc Test_GzipHandler_CompressableType(t *testing.T) {\n\tserver := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes))\n\n\tassertEqual := assert.Equal(t)\n\n\tr, err := http.NewRequest(\"GET\", server.URL, nil)\n\tassertEqual(err, nil)\n\tr.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := http.DefaultClient.Do(r)\n\tassertEqual(err, nil)\n\n\tassertEqual(resp.Header.Get(\"Content-Type\"), \"text/plain; charset=utf-8\")\n\tassertEqual(resp.Header.Get(\"Content-Encoding\"), \"gzip\")\n\n\tgzBytes, err := io.ReadAll(resp.Body)\n\tassertEqual(err, nil)\n\tassertEqual(resp.Header.Get(\"Content-Length\"), strconv.Itoa(len(gzBytes)))\n\n\treader, err := gzip.NewReader(bytes.NewBuffer(gzBytes))\n\tassertEqual(err, nil)\n\tdefer reader.Close()\n\n\tbytes, err := io.ReadAll(reader)\n\tassertEqual(err, nil)\n\n\tassertEqual(string(bytes), \"Hello World\")\n}\n\nfunc Test_GzipHandler_NotCompressingTwice(t *testing.T) {\n\tserver := httptest.NewServer(NewGzipHandler(test_already_compressed_handler(), contentTypes))\n\n\tassertEqual := assert.Equal(t)\n\n\tr, err := http.NewRequest(\"GET\", server.URL, nil)\n\tassertEqual(err, nil)\n\tr.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := http.DefaultClient.Do(r)\n\tassertEqual(err, nil)\n\n\tassertEqual(resp.Header.Get(\"Content-Encoding\"), \"gzip\")\n\n\treader, err := gzip.NewReader(resp.Body)\n\tassertEqual(err, nil)\n\tdefer reader.Close()\n\n\tbytes, err := io.ReadAll(reader)\n\tassertEqual(err, nil)\n\n\tassertEqual(string(bytes), \"Hello World\")\n}\n\nfunc Test_GzipHandler_CompressableType_NoAccept(t *testing.T) {\n\tserver := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes))\n\n\tassertEqual := assert.Equal(t)\n\n\tr, err := http.NewRequest(\"GET\", server.URL, nil)\n\tassertEqual(err, nil)\n\tr.Header.Set(\"Accept-Encoding\", \"none\")\n\n\tresp, err := http.DefaultClient.Do(r)\n\tassertEqual(err, nil)\n\n\tassertEqual(resp.Header.Get(\"Content-Encoding\"), \"\")\n\n\tbytes, err := io.ReadAll(resp.Body)\n\tassertEqual(err, nil)\n\n\tassertEqual(string(bytes), \"Hello World\")\n}\n\nfunc Test_GzipHandler_NonCompressableType(t *testing.T) {\n\tserver := httptest.NewServer(NewGzipHandler(test_binary_handler(), contentTypes))\n\n\tassertEqual := assert.Equal(t)\n\n\tr, err := http.NewRequest(\"GET\", server.URL, nil)\n\tassertEqual(err, nil)\n\tr.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := http.DefaultClient.Do(r)\n\tassertEqual(err, nil)\n\n\tassertEqual(resp.Header.Get(\"Content-Encoding\"), \"\")\n\n\tbytes, err := io.ReadAll(resp.Body)\n\tassertEqual(err, nil)\n\n\tassertEqual(bytes, []byte{42})\n}\n\nfunc test_text_handler() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tb := []byte(\"Hello World\")\n\t\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(b)))\n\t\tw.Write(b)\n\t})\n}\n\nfunc test_binary_handler() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"image/jpg\")\n\t\tw.Write([]byte{42})\n\t})\n}\n\nfunc test_already_compressed_handler() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\tgzWriter := gzip.NewWriter(w)\n\t\tgzWriter.Write([]byte(\"Hello World\"))\n\t\tgzWriter.Close()\n\t})\n}\n"
  },
  {
    "path": "proxy/http_handler.go",
    "content": "package proxy\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"time\"\n)\n\n// StatusClientClosedRequest non-standard HTTP status code for client disconnection\nconst StatusClientClosedRequest = 499\n\nfunc newHTTPProxy(target *url.URL, tr http.RoundTripper, flush time.Duration) http.Handler {\n\treturn &httputil.ReverseProxy{\n\t\t// this is a simplified director function based on the\n\t\t// httputil.NewSingleHostReverseProxy() which does not\n\t\t// mangle the request and target URL since the target\n\t\t// URL is already in the correct format.\n\t\tDirector: func(req *http.Request) {\n\t\t\treq.URL.Scheme = target.Scheme\n\t\t\treq.URL.Host = target.Host\n\t\t\treq.URL.Path = target.Path\n\t\t\treq.URL.RawQuery = target.RawQuery\n\t\t\tif _, ok := req.Header[\"User-Agent\"]; !ok {\n\t\t\t\t// explicitly disable User-Agent so it's not set to default value\n\t\t\t\treq.Header.Set(\"User-Agent\", \"\")\n\t\t\t}\n\t\t},\n\t\tFlushInterval: flush,\n\t\tTransport:     tr,\n\t\tErrorHandler:  httpProxyErrorHandler,\n\t}\n}\n\nfunc httpProxyErrorHandler(w http.ResponseWriter, r *http.Request, err error) {\n\t// According to https://golang.org/src/net/http/httputil/reverseproxy.go#L74, Go will return a 502 (Bad Gateway) StatusCode by default if no ErrorHandler is provided\n\t// If a \"context canceled\" error is returned by the http.Request handler this means the client closed the connection before getting a response\n\t// So we are changing the StatusCode on these situations to the non-standard 499 (Client Closed Request)\n\n\tstatusCode := http.StatusInternalServerError\n\n\tif e, ok := err.(net.Error); ok {\n\t\tif e.Timeout() {\n\t\t\tstatusCode = http.StatusGatewayTimeout\n\t\t} else {\n\t\t\tstatusCode = http.StatusBadGateway\n\t\t}\n\t} else if err == io.EOF {\n\t\tstatusCode = http.StatusBadGateway\n\t} else if err == context.Canceled {\n\t\tstatusCode = StatusClientClosedRequest\n\t}\n\n\tw.WriteHeader(statusCode)\n\t// Theres nothing we can do if the client closes the connection and logging the \"context canceled\" errors will just add noise to the error log\n\t// Note: The access_log will still log the 499 response status codes\n\tif statusCode != StatusClientClosedRequest {\n\t\tlog.Print(\"[ERROR] \", err)\n\t}\n}\n"
  },
  {
    "path": "proxy/http_headers.go",
    "content": "package proxy\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strings\"\n\n\t\"github.com/fabiolb/fabio/config\"\n)\n\n// addResponseHeaders adds/updates headers in the response\nfunc addResponseHeaders(w http.ResponseWriter, r *http.Request, cfg config.Proxy) {\n\tif r.TLS != nil && cfg.STSHeader.MaxAge > 0 {\n\t\tsts := \"max-age=\" + i32toa(int32(cfg.STSHeader.MaxAge))\n\t\tif cfg.STSHeader.Subdomains {\n\t\t\tsts += \"; includeSubdomains\"\n\t\t}\n\t\tif cfg.STSHeader.Preload {\n\t\t\tsts += \"; preload\"\n\t\t}\n\t\tw.Header().Set(\"Strict-Transport-Security\", sts)\n\t}\n}\n\nvar protectHeaders = map[string]bool{\n\t\"Forwarded\":          true,\n\t\"X-Forwarded-For\":    true,\n\t\"X-Forwarded-Host\":   true,\n\t\"X-Forwarded-Port\":   true,\n\t\"X-Forwarded-Proto\":  true,\n\t\"X-Forwarded-Prefix\": true,\n\t\"X-Real-Ip\":          true,\n}\n\n// addHeaders adds/updates headers in request\n//\n// * add/update `Forwarded` header\n// * add X-Forwarded-Proto header, if not present\n// * add X-Real-Ip, if not present\n// * ClientIPHeader != \"\": Set header with that name to <remote ip>\n// * TLS connection: Set header with name from `cfg.TLSHeader` to `cfg.TLSHeaderValue`\nfunc addHeaders(r *http.Request, cfg config.Proxy, stripPath string) error {\n\tremoteIP, _, err := net.SplitHostPort(r.RemoteAddr)\n\tif err != nil {\n\t\treturn errors.New(\"cannot parse \" + r.RemoteAddr)\n\t}\n\n\t// exclude headers from Connection rules.\n\tvar conHeaders []string\n\tfor _, s := range r.Header.Values(\"Connection\") {\n\t\tfor _, p := range strings.Split(s, \",\") {\n\t\t\tp = strings.TrimSpace(p)\n\t\t\tif !protectHeaders[textproto.CanonicalMIMEHeaderKey(p)] {\n\t\t\t\tconHeaders = append(conHeaders, p)\n\t\t\t}\n\t\t}\n\t}\n\n\tr.Header.Del(\"Connection\")\n\tif len(conHeaders) > 0 {\n\t\tr.Header.Set(\"Connection\", strings.Join(conHeaders, \", \"))\n\t}\n\n\t// set configurable ClientIPHeader\n\t// X-Real-Ip is set later and X-Forwarded-For is set\n\t// by the Go HTTP reverse proxy.\n\tif cfg.ClientIPHeader != \"\" &&\n\t\tcfg.ClientIPHeader != \"X-Forwarded-For\" &&\n\t\tcfg.ClientIPHeader != \"X-Real-Ip\" {\n\t\tr.Header.Set(cfg.ClientIPHeader, remoteIP)\n\t}\n\n\tif r.Header.Get(\"X-Real-Ip\") == \"\" {\n\t\tr.Header.Set(\"X-Real-Ip\", remoteIP)\n\t}\n\n\t// set the X-Forwarded-For header for websocket\n\t// connections since they aren't handled by the\n\t// http proxy which sets it.\n\tws := r.Header.Get(\"Upgrade\") == \"websocket\"\n\tif ws {\n\t\tclientIP := remoteIP\n\t\t// If we aren't the first proxy retain prior\n\t\t// X-Forwarded-For information as a comma+space\n\t\t// separated list and fold multiple headers into one.\n\t\tprior, ok := r.Header[\"X-Forwarded-For\"]\n\t\tomit := ok && prior == nil // Issue 38079: nil now means don't populate the header\n\t\tif len(prior) > 0 {\n\t\t\tclientIP = strings.Join(prior, \", \") + \", \" + clientIP\n\t\t}\n\t\tif !omit {\n\t\t\tr.Header.Set(\"X-Forwarded-For\", clientIP)\n\t\t}\n\t}\n\n\t// Issue #133: Setting the X-Forwarded-Proto header to\n\t// anything other than 'http' or 'https' breaks java\n\t// websocket clients which use java.net.URL for composing\n\t// the forwarded URL. Since X-Forwarded-Proto is not\n\t// specified the common practice is to set it to either\n\t// 'http' for 'ws' and 'https' for 'wss' connections.\n\tproto := scheme(r)\n\tif r.Header.Get(\"X-Forwarded-Proto\") == \"\" {\n\t\tswitch proto {\n\t\tcase \"ws\":\n\t\t\tr.Header.Set(\"X-Forwarded-Proto\", \"http\")\n\t\tcase \"wss\":\n\t\t\tr.Header.Set(\"X-Forwarded-Proto\", \"https\")\n\t\tdefault:\n\t\t\tr.Header.Set(\"X-Forwarded-Proto\", proto)\n\t\t}\n\t}\n\n\tif r.Header.Get(\"X-Forwarded-Port\") == \"\" {\n\t\tr.Header.Set(\"X-Forwarded-Port\", localPort(r))\n\t}\n\n\tif r.Header.Get(\"X-Forwarded-Host\") == \"\" && r.Host != \"\" {\n\t\tr.Header.Set(\"X-Forwarded-Host\", r.Host)\n\t}\n\n\tif stripPath != \"\" {\n\t\tr.Header.Set(\"X-Forwarded-Prefix\", stripPath)\n\t}\n\n\tfwd := r.Header.Get(\"Forwarded\")\n\tif fwd == \"\" {\n\t\tfwd = \"for=\" + remoteIP + \"; proto=\" + proto\n\t}\n\tif cfg.LocalIP != \"\" {\n\t\tfwd += \"; by=\" + cfg.LocalIP\n\t}\n\tif r.Proto != \"\" {\n\t\tfwd += \"; httpproto=\" + strings.ToLower(r.Proto)\n\t}\n\tif r.TLS != nil && r.TLS.Version > 0 {\n\t\tv := tlsver[r.TLS.Version]\n\t\tif v == \"\" {\n\t\t\tv = uint16base16(r.TLS.Version)\n\t\t}\n\t\tfwd += \"; tlsver=\" + v\n\t}\n\tif r.TLS != nil && r.TLS.CipherSuite != 0 {\n\t\tfwd += \"; tlscipher=\" + uint16base16(r.TLS.CipherSuite)\n\t}\n\tr.Header.Set(\"Forwarded\", fwd)\n\n\tif cfg.TLSHeader != \"\" {\n\t\tif r.TLS != nil {\n\t\t\tr.Header.Set(cfg.TLSHeader, cfg.TLSHeaderValue)\n\t\t} else {\n\t\t\tr.Header.Del(cfg.TLSHeader)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar tlsver = map[uint16]string{\n\ttls.VersionTLS10: \"tls10\",\n\ttls.VersionTLS11: \"tls11\",\n\ttls.VersionTLS12: \"tls12\",\n}\n\nvar digit16 = []byte(\"0123456789abcdef\")\n\n// uint16base64 is a faster version of fmt.Sprintf(\"0x%04x\", n)\n//\n// BenchmarkUint16Base16/fmt.Sprintf-8         \t10000000\t       154 ns/op\t       8 B/op\t       2 allocs/op\n// BenchmarkUint16Base16/uint16base16-8        \t50000000\t        35.0 ns/op\t       8 B/op\t       1 allocs/op\nfunc uint16base16(n uint16) string {\n\tb := []byte(\"0x0000\")\n\tb[5] = digit16[n&0x000f]\n\tb[4] = digit16[n&0x00f0>>4]\n\tb[3] = digit16[n&0x0f00>>8]\n\tb[2] = digit16[n&0xf000>>12]\n\treturn string(b)\n}\n\n// i32toa is a faster implementation of strconv.Itoa() without importing another library\n// https://stackoverflow.com/a/39444005\nfunc i32toa(n int32) string {\n\tbuf := [11]byte{}\n\tpos := len(buf)\n\ti := int64(n)\n\tsigned := i < 0\n\tif signed {\n\t\ti = -i\n\t}\n\tfor {\n\t\tpos--\n\t\tbuf[pos], i = '0'+byte(i%10), i/10\n\t\tif i == 0 {\n\t\t\tif signed {\n\t\t\t\tpos--\n\t\t\t\tbuf[pos] = '-'\n\t\t\t}\n\t\t\treturn string(buf[pos:])\n\t\t}\n\t}\n}\n\n// scheme derives the request scheme used on the initial\n// request first from headers and then from the connection\n// using the following heuristic:\n//\n// If either X-Forwarded-Proto or Forwarded is set then use\n// its value to set the other header. If both headers are\n// set do not modify the protocol. If none are set derive\n// the protocol from the connection.\nfunc scheme(r *http.Request) string {\n\txfp := r.Header.Get(\"X-Forwarded-Proto\")\n\tfwd := r.Header.Get(\"Forwarded\")\n\tswitch {\n\tcase xfp != \"\" && fwd == \"\":\n\t\treturn xfp\n\n\tcase fwd != \"\" && xfp == \"\":\n\t\tp := strings.SplitAfterN(fwd, \"proto=\", 2)\n\t\tif len(p) == 1 {\n\t\t\tbreak\n\t\t}\n\t\tn := strings.IndexRune(p[1], ';')\n\t\tif n >= 0 {\n\t\t\treturn p[1][:n]\n\t\t}\n\t\treturn p[1]\n\t}\n\n\tws := r.Header.Get(\"Upgrade\") == \"websocket\"\n\tswitch {\n\tcase ws && r.TLS != nil:\n\t\treturn \"wss\"\n\tcase ws && r.TLS == nil:\n\t\treturn \"ws\"\n\tcase r.TLS != nil:\n\t\treturn \"https\"\n\tdefault:\n\t\treturn \"http\"\n\t}\n}\n\nfunc localPort(r *http.Request) string {\n\tif r == nil {\n\t\treturn \"\"\n\t}\n\tn := strings.Index(r.Host, \":\")\n\tif n > 0 && n < len(r.Host)-1 {\n\t\treturn r.Host[n+1:]\n\t}\n\tif r.TLS != nil {\n\t\treturn \"443\"\n\t}\n\treturn \"80\"\n}\n"
  },
  {
    "path": "proxy/http_headers_test.go",
    "content": "package proxy\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/pascaldekloe/goe/verify\"\n)\n\nfunc TestAddHeaders(t *testing.T) {\n\ttests := []struct {\n\t\tdesc  string\n\t\tr     *http.Request\n\t\tcfg   config.Proxy\n\t\tstrip string\n\t\thdrs  http.Header\n\t\terr   string\n\t}{\n\t\t{\"error\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4\"},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{},\n\t\t\t\"cannot parse 1.2.3.4\",\n\t\t},\n\n\t\t{\"http request\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\"},\n\t\t\tconfig.Proxy{},\n\t\t\t\"/foo\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":          []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\":  []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":   []string{\"80\"},\n\t\t\t\t\"X-Forwarded-Prefix\": []string{\"/foo\"},\n\t\t\t\t\"X-Real-Ip\":          []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"https request\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"https request hack\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Connection\": {\"keep-alive\", \"X-Real-Ip\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Connection\":        []string{\"keep-alive\"},\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"ws request\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Upgrade\": {\"websocket\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=ws\"},\n\t\t\t\t\"Upgrade\":           []string{\"websocket\"},\n\t\t\t\t\"X-Forwarded-For\":   []string{\"1.2.3.4\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"wss request\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Upgrade\": {\"websocket\"}}, TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=wss\"},\n\t\t\t\t\"Upgrade\":           []string{\"websocket\"},\n\t\t\t\t\"X-Forwarded-For\":   []string{\"1.2.3.4\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set client ip header\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\"},\n\t\t\tconfig.Proxy{ClientIPHeader: \"Client-IP\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Client-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set Forwarded with localIP\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\"},\n\t\t\tconfig.Proxy{LocalIP: \"5.6.7.8\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http; by=5.6.7.8\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set Forwarded with localIP for https\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{LocalIP: \"5.6.7.8\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https; by=5.6.7.8\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set httpproto, tlsver and tlscipher on Forwarded for https\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Proto: \"HTTP/1.1\", TLS: &tls.ConnectionState{Version: tls.VersionTLS10, CipherSuite: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https; httpproto=http/1.1; tlsver=tls10; tlscipher=0xc023\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set httpproto on Forwarded\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Proto: \"HTTP/1.1\"},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http; httpproto=http/1.1\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"extend Forwarded with localIP\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Forwarded\": {\"for=9.9.9.9; proto=http; by=8.8.8.8\"}}},\n\t\t\tconfig.Proxy{LocalIP: \"5.6.7.8\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=9.9.9.9; proto=http; by=8.8.8.8; by=5.6.7.8\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set tls header\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{TLSHeader: \"Secure\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https\"},\n\t\t\t\t\"Secure\":            []string{\"\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set tls header with value\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{TLSHeader: \"Secure\", TLSHeaderValue: \"true\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https\"},\n\t\t\t\t\"Secure\":            []string{\"true\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"overwrite tls header for https, when set\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Secure\": []string{\"on\"}}, TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{TLSHeader: \"Secure\", TLSHeaderValue: \"true\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https\"},\n\t\t\t\t\"Secure\":            []string{\"true\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"443\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"drop tls header for http, when set\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Secure\": []string{\"on\"}}},\n\t\t\tconfig.Proxy{TLSHeader: \"Secure\", TLSHeaderValue: \"true\"},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"do not overwrite X-Forwarded-Proto, if present\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"X-Forwarded-Proto\": {\"some value\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=some value\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"some value\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set scheme from X-Forwarded-Proto, if present and Forwarded is missing\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"X-Forwarded-Proto\": {\"some value\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=some value\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"some value\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set scheme from Forwarded, if present and X-Forwarded-Proto is missing\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"Forwarded\": {\"for=1.2.3.4; proto=some value\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=some value\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"some value\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"do not modify scheme when both Forwarded and X-Forwarded-Proto are present\",\n\t\t\t&http.Request{\n\t\t\t\tRemoteAddr: \"1.2.3.4:5555\",\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Forwarded\":         {\"for=1.2.3.4; proto=some value\"},\n\t\t\t\t\t\"X-Forwarded-Proto\": {\"other value\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=some value\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"other value\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set X-Forwarded-Port from Host\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Host: \"5.6.7.8:1234\"},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Host\":  []string{\"5.6.7.8:1234\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"1234\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set X-Forwarded-Port from Host for https\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Host: \"5.6.7.8:1234\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=https\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"https\"},\n\t\t\t\t\"X-Forwarded-Host\":  []string{\"5.6.7.8:1234\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"1234\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"do not overwrite X-Forwarded-Port header, if present\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"X-Forwarded-Port\": {\"4444\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"4444\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set X-Forwarded-Host from Host\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Host: \"5.6.7.8:1234\"},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Host\":  []string{\"5.6.7.8:1234\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"1234\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"do not overwrite X-Forwarded-Host, if present\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Host: \"5.6.7.8:1234\", Header: http.Header{\"X-Forwarded-Host\": {\"9.10.11.12:1234\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Host\":  []string{\"9.10.11.12:1234\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"1234\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"1.2.3.4\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"do not overwrite X-Real-Ip, if present\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", Header: http.Header{\"X-Real-Ip\": {\"6.6.6.6\"}}},\n\t\t\tconfig.Proxy{},\n\t\t\t\"\",\n\t\t\thttp.Header{\n\t\t\t\t\"Forwarded\":         []string{\"for=1.2.3.4; proto=http\"},\n\t\t\t\t\"X-Forwarded-Proto\": []string{\"http\"},\n\t\t\t\t\"X-Forwarded-Port\":  []string{\"80\"},\n\t\t\t\t\"X-Real-Ip\":         []string{\"6.6.6.6\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tif tt.r.Header == nil {\n\t\t\t\ttt.r.Header = http.Header{}\n\t\t\t}\n\n\t\t\terr := addHeaders(tt.r, tt.cfg, tt.strip)\n\t\t\tif err != nil {\n\t\t\t\tif got, want := err.Error(), tt.err; got != want {\n\t\t\t\t\tt.Fatalf(\"%d: %s\\ngot  %q\\nwant %q\", i, tt.desc, got, want)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.err != \"\" {\n\t\t\t\tt.Fatalf(\"%d: got nil want %q\", i, tt.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgot, want := tt.r.Header, tt.hdrs\n\t\t\tverify.Values(t, \"\", got, want)\n\t\t})\n\t}\n}\n\nfunc TestAddResponseHeaders(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\tr    *http.Request\n\t\tcfg  config.Proxy\n\t\thdrs http.Header\n\t\terr  string\n\t}{\n\t\t{\"set Strict-Transport-Security for TLS, if MaxAge greater than 0\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},\n\t\t\thttp.Header{\n\t\t\t\t\"Strict-Transport-Security\": []string{\"max-age=31536000\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"set Strict-Transport-Security for TLS, if MaxAge greater than 0 with options\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\", TLS: &tls.ConnectionState{}},\n\t\t\tconfig.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000, Preload: true, Subdomains: true}},\n\t\t\thttp.Header{\n\t\t\t\t\"Strict-Transport-Security\": []string{\"max-age=31536000; includeSubdomains; preload\"},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\n\t\t{\"skip Strict-Transport-Security for non-TLS, if MaxAge greater than 0\",\n\t\t\t&http.Request{RemoteAddr: \"1.2.3.4:5555\"},\n\t\t\tconfig.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},\n\t\t\thttp.Header{},\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tif tt.r.Header == nil {\n\t\t\t\ttt.r.Header = http.Header{}\n\t\t\t}\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\taddResponseHeaders(w, tt.r, tt.cfg)\n\n\t\t\tif tt.err != \"\" {\n\t\t\t\tt.Fatalf(\"%d: got nil want %q\", i, tt.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresp := w.Result()\n\t\t\tgot, want := resp.Header, tt.hdrs\n\t\t\tverify.Values(t, \"\", got, want)\n\t\t})\n\t}\n}\n\nfunc TestLocalPort(t *testing.T) {\n\ttests := []struct {\n\t\tr    *http.Request\n\t\tport string\n\t}{\n\t\t{nil, \"\"},\n\t\t{&http.Request{Host: \"\"}, \"80\"},\n\t\t{&http.Request{Host: \":\"}, \"80\"},\n\t\t{&http.Request{Host: \"1.2.3.4:5678\"}, \"5678\"},\n\t\t{&http.Request{Host: \"1.2.3.4\"}, \"80\"},\n\t\t{&http.Request{Host: \"1.2.3.4\", TLS: &tls.ConnectionState{}}, \"443\"},\n\t\t{&http.Request{Host: \"1.2.3.4:\"}, \"80\"},\n\t\t{&http.Request{Host: \"1.2.3.4:\", TLS: &tls.ConnectionState{}}, \"443\"},\n\t}\n\n\tfor i, tt := range tests {\n\t\tif got, want := localPort(tt.r), tt.port; got != want {\n\t\t\tt.Errorf(\"%d: got %q want %q\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestUint16Base16(t *testing.T) {\n\tfor i := range uint16(9999) {\n\t\tif got, want := uint16base16(i), fmt.Sprintf(\"0x%04x\", i); got != want {\n\t\t\tt.Fatalf(\"got %q for %04x want %q\", got, i, want)\n\t\t}\n\t}\n}\n\nfunc BenchmarkUint16Base16(b *testing.B) {\n\t// keep a variable outside of the tests so that the compiler doesn't\n\t// optimize the body of the loop away.\n\tvar s string\n\tb.Run(\"fmt.Sprintf\", func(b *testing.B) {\n\t\tfor i := range b.N {\n\t\t\ts = fmt.Sprintf(\"0x%04x\", uint16(i))\n\t\t}\n\t})\n\tb.Run(\"uint16base16\", func(b *testing.B) {\n\t\tfor i := range b.N {\n\t\t\ts = uint16base16(uint16(i))\n\t\t}\n\t})\n\tb.Logf(\"BenchmarkUint16Base16 %v\", s) // use the var to make go1.10 vet happy\n}\n"
  },
  {
    "path": "proxy/http_integration_test.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/logger\"\n\t\"github.com/fabiolb/fabio/noroute\"\n\t\"github.com/fabiolb/fabio/proxy/internal\"\n\t\"github.com/fabiolb/fabio/route\"\n\t\"github.com/pascaldekloe/goe/verify\"\n)\n\nconst (\n\t// helper constants for the Lookup function\n\tglobEnabled  = false\n\tglobDisabled = true\n)\n\n// Global GlobCache for Testing\nvar globCache = route.NewGlobCache(1000)\n\nconst (\n\tlegitHeader1 = \"Legit-Header1\"\n\tlegitHeader2 = \"Legit-Header2\"\n)\n\nfunc TestProxyProducesCorrectXForwardedSomethingHeader(t *testing.T) {\n\tvar hdr = make(http.Header)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\thdr = r.Header\n\t}))\n\tdefer server.Close()\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{LocalIP: \"1.1.1.1\", ClientIPHeader: \"X-Forwarded-For\"},\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn &route.Target{URL: mustParse(server.URL)}\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\treq, _ := http.NewRequest(\"GET\", proxy.URL, nil)\n\treq.Host = \"foo.com\"\n\treq.Header.Set(\"X-Forwarded-For\", \"3.3.3.3\")\n\treq.Header.Set(legitHeader1, \"asdf\")\n\treq.Header.Set(legitHeader2, \"qwerty\")\n\treq.Header.Set(\"Connection\",\n\t\tfmt.Sprintf(\"keep-alive, x-forwarded-for, x-forwarded-host, %s, %s\",\n\t\t\tstrings.ToLower(legitHeader1), strings.ToLower(legitHeader2)))\n\tmustDo(req)\n\n\tif got, want := hdr.Get(\"X-Forwarded-For\"), \"3.3.3.3, 127.0.0.1\"; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := hdr.Get(\"X-Forwarded-Host\"), \"foo.com\"; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := hdr.Get(legitHeader1), \"\"; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := hdr.Get(legitHeader2), \"\"; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n}\n\nfunc TestProxyRequestIDHeader(t *testing.T) {\n\tgot := \"not called\"\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tgot = r.Header.Get(\"X-Request-ID\")\n\t}))\n\tdefer server.Close()\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{RequestID: \"X-Request-Id\"},\n\t\tTransport: http.DefaultTransport,\n\t\tUUID:      func() string { return \"f47ac10b-58cc-0372-8567-0e02b2c3d479\" },\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn &route.Target{URL: mustParse(server.URL)}\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\treq, _ := http.NewRequest(\"GET\", proxy.URL, nil)\n\tmustDo(req)\n\n\tif want := \"f47ac10b-58cc-0372-8567-0e02b2c3d479\"; got != want {\n\t\tt.Errorf(\"got %v, but want %v\", got, want)\n\t}\n}\n\nfunc TestProxySTSHeader(t *testing.T) {\n\tserver := httptest.NewServer(okHandler)\n\tdefer server.Close()\n\n\tproxy := httptest.NewTLSServer(&HTTPProxy{\n\t\tConfig: config.Proxy{\n\t\t\tSTSHeader: config.STSHeader{\n\t\t\t\tMaxAge:     31536000,\n\t\t\t\tSubdomains: true,\n\t\t\t\tPreload:    true,\n\t\t\t},\n\t\t},\n\t\tTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn &route.Target{URL: mustParse(server.URL)}\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tclient := http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: tlsInsecureConfig(),\n\t\t},\n\t}\n\tresp, err := client.Get(proxy.URL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif got, want := resp.Header.Get(\"Strict-Transport-Security\"),\n\t\t\"max-age=31536000; includeSubdomains; preload\"; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n}\n\nfunc TestProxyChecksHeaderForAccessRules(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"OK\")\n\t}))\n\tdefer server.Close()\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{},\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttgt := &route.Target{\n\t\t\t\tURL:  mustParse(server.URL),\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:127.0.0.0/8,ip:fe80::/10,ip:::1\"},\n\t\t\t}\n\t\t\ttgt.ProcessAccessRules()\n\t\t\treturn tgt\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\treq, _ := http.NewRequest(\"GET\", proxy.URL, nil)\n\treq.Header.Set(\"X-Forwarded-For\", \"1.2.3.4\")\n\tresp, _ := mustDo(req)\n\n\tif got, want := resp.StatusCode, http.StatusForbidden; got != want {\n\t\tt.Errorf(\"got %v want %v\", got, want)\n\t}\n}\n\nfunc TestProxyNoRouteHTML(t *testing.T) {\n\twant := \"<html>503</html>\"\n\tnoroute.SetHTML(want)\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: http.DefaultTransport,\n\t\tLookup:    func(*http.Request) *route.Target { return nil },\n\t})\n\tdefer proxy.Close()\n\n\t_, got := mustGet(proxy.URL)\n\tif !bytes.Equal(got, []byte(want)) {\n\t\tt.Fatalf(\"got %s want %s\", got, want)\n\t}\n}\n\nfunc TestProxyNoRouteStatus(t *testing.T) {\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{NoRouteStatus: 999},\n\t\tTransport: http.DefaultTransport,\n\t\tLookup:    func(*http.Request) *route.Target { return nil },\n\t})\n\tdefer proxy.Close()\n\n\tresp, _ := mustGet(proxy.URL)\n\tif got, want := resp.StatusCode, 999; got != want {\n\t\tt.Fatalf(\"got %d want %d\", got, want)\n\t}\n}\n\nfunc TestProxyStripsPath(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/bar\":\n\t\t\tw.Write([]byte(\"OK\"))\n\t\tdefault:\n\t\t\tw.WriteHeader(404)\n\t\t}\n\t}))\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add mock /foo/bar \" + server.URL + ` opts \"strip=/foo\"`))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tresp, body := mustGet(proxy.URL + \"/foo/bar\")\n\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t}\n\tif got, want := string(body), \"OK\"; got != want {\n\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t}\n}\n\nfunc TestProxyPrependsPath(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/foo/bar\":\n\t\t\tw.Write([]byte(\"OK\"))\n\t\tdefault:\n\t\t\tw.WriteHeader(404)\n\t\t}\n\t}))\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add mock /bar \" + server.URL + ` opts \"prepend=/foo\"`))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tresp, body := mustGet(proxy.URL + \"/bar\")\n\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t}\n\tif got, want := string(body), \"OK\"; got != want {\n\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t}\n}\n\nfunc TestProxyHost(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, r.Host)\n\t}))\n\n\t// create a static route table so that we can see the effect\n\t// of round robin distribution. The other tests generate the\n\t// route table on the fly since order does not matter to them.\n\troutes := \"route add mock /hostdst http://a.com/ opts \\\"host=dst\\\"\\n\"\n\troutes += \"route add mock /hostcustom http://a.com/ opts \\\"host=foo.com\\\"\\n\"\n\troutes += \"route add mock /hostcustom http://b.com/ opts \\\"host=bar.com\\\"\\n\"\n\troutes += \"route add mock / http://a.com/\"\n\ttbl, _ := route.NewTable(bytes.NewBufferString(routes))\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: &http.Transport{\n\t\t\tDial: func(network, _ string) (net.Conn, error) {\n\t\t\t\taddr := server.URL[len(\"http://\"):]\n\t\t\t\treturn net.Dial(network, addr)\n\t\t\t},\n\t\t},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tcheck := func(t *testing.T, uri, host string) {\n\t\tresp, body := mustGet(proxy.URL + uri)\n\t\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t\t}\n\t\tif got, want := string(body), host; got != want {\n\t\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t\t}\n\t}\n\n\tproxyHost := proxy.URL[len(\"http://\"):]\n\n\t// test that for 'host=dst' the Host header is set to the hostname of the\n\t// target, in this case 'a.com'\n\tt.Run(\"host eq dst\", func(t *testing.T) { check(t, \"/hostdst\", \"a.com\") })\n\n\t// test that without a 'host' option no Host header is set\n\tt.Run(\"no host\", func(t *testing.T) { check(t, \"/\", proxyHost) })\n\n\t// 1. Test that a host header is set when the 'host' option is used.\n\t//\n\t// 2. Test that the host header is set per target, i.e. that different\n\t//    targets can have different 'host' options.\n\t//\n\t//    The proxy is configured to use \"rr\" (round-robin) distribution\n\t//    for the requests. Therefore, requests to '/hostcustom' will be\n\t//    sent to the two different targets in alternating order.\n\tt.Run(\"host is custom\", func(t *testing.T) {\n\t\tcheck(t, \"/hostcustom\", \"foo.com\")\n\t\tcheck(t, \"/hostcustom\", \"bar.com\")\n\t})\n}\n\nfunc TestHostRedirect(t *testing.T) {\n\troutes := \"route add https-redir *:80 https://$host$path opts \\\"redirect=301\\\"\\n\"\n\n\ttbl, _ := route.NewTable(bytes.NewBufferString(routes))\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\tr.Host = \"c.com\"\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\ttests := []struct {\n\t\treq      string\n\t\twantCode int\n\t\twantLoc  string\n\t}{\n\t\t{req: \"/baz\", wantCode: 301, wantLoc: \"https://c.com/baz\"},\n\t}\n\n\thttp.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t// do not follow redirects\n\t\treturn http.ErrUseLastResponse\n\t}\n\n\tfor _, tt := range tests {\n\t\tresp, _ := mustGet(proxy.URL + tt.req)\n\t\tif resp.StatusCode != tt.wantCode {\n\t\t\tt.Errorf(\"got status code %d, want %d\", resp.StatusCode, tt.wantCode)\n\t\t}\n\t\tgotLoc, _ := resp.Location()\n\t\tif gotLoc.String() != tt.wantLoc {\n\t\t\tt.Errorf(\"got location %s, want %s\", gotLoc, tt.wantLoc)\n\t\t}\n\t}\n}\n\nfunc TestPathRedirect(t *testing.T) {\n\troutes := \"route add mock / http://a.com/$path opts \\\"redirect=301\\\"\\n\"\n\troutes += \"route add mock /foo http://a.com/abc opts \\\"redirect=301\\\"\\n\"\n\troutes += \"route add mock /bar http://b.com/$path opts \\\"redirect=302 strip=/bar\\\"\\n\"\n\ttbl, _ := route.NewTable(bytes.NewBufferString(routes))\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\ttests := []struct {\n\t\treq      string\n\t\twantCode int\n\t\twantLoc  string\n\t}{\n\t\t{req: \"/\", wantCode: 301, wantLoc: \"http://a.com/\"},\n\t\t{req: \"/aaa/bbb\", wantCode: 301, wantLoc: \"http://a.com/aaa/bbb\"},\n\t\t{req: \"/foo\", wantCode: 301, wantLoc: \"http://a.com/abc\"},\n\t\t{req: \"/bar\", wantCode: 302, wantLoc: \"http://b.com/\"},\n\t\t{req: \"/bar/aaa\", wantCode: 302, wantLoc: \"http://b.com/aaa\"},\n\t}\n\n\thttp.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\t// do not follow redirects\n\t\treturn http.ErrUseLastResponse\n\t}\n\n\tfor _, tt := range tests {\n\t\tresp, _ := mustGet(proxy.URL + tt.req)\n\t\tif resp.StatusCode != tt.wantCode {\n\t\t\tt.Errorf(\"got status code %d, want %d\", resp.StatusCode, tt.wantCode)\n\t\t}\n\t\tgotLoc, _ := resp.Location()\n\t\tif gotLoc.String() != tt.wantLoc {\n\t\t\tt.Errorf(\"got location %s, want %s\", gotLoc, tt.wantLoc)\n\t\t}\n\t}\n}\n\nfunc TestProxyLogOutput(t *testing.T) {\n\tt.Run(\"uncompressed response\", func(t *testing.T) {\n\t\ttestProxyLogOutput(t, 73, config.Proxy{})\n\t})\n\tt.Run(\"compression enabled but no match\", func(t *testing.T) {\n\t\ttestProxyLogOutput(t, 73, config.Proxy{\n\t\t\tGZIPContentTypes: regexp.MustCompile(`^$`),\n\t\t})\n\t})\n\tt.Run(\"compression enabled and active\", func(t *testing.T) {\n\t\ttestProxyLogOutput(t, 28, config.Proxy{\n\t\t\tGZIPContentTypes: regexp.MustCompile(`.*`),\n\t\t})\n\t})\n}\n\nfunc testProxyLogOutput(t *testing.T, bodySize int, cfg config.Proxy) {\n\tt.Helper()\n\n\t// build a format string from all log fields and one header field\n\tfields := []string{\"header.X-Foo:$header.X-Foo\"}\n\tfor _, k := range logger.Fields {\n\t\tfields = append(fields, k[1:]+\":\"+k)\n\t}\n\tformat := strings.Join(fields, \";\")\n\n\t// create a logger\n\tvar b bytes.Buffer\n\tl, err := logger.New(&b, format)\n\tif err != nil {\n\t\tt.Fatal(\"logger.New: \", err)\n\t}\n\n\t// create an upstream server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, \"foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\")\n\t}))\n\tdefer server.Close()\n\n\t// create a proxy handler with mocked time\n\ttm := time.Date(2016, 1, 1, 0, 0, 0, 12345678, time.UTC)\n\tproxyHandler := &HTTPProxy{\n\t\tConfig: cfg,\n\t\tTime: func() time.Time {\n\t\t\tdefer func() { tm = tm.Add(1111111111 * time.Nanosecond) }()\n\t\t\treturn tm\n\t\t},\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn &route.Target{\n\t\t\t\tService: \"svc-a\",\n\t\t\t\tURL:     mustParse(server.URL),\n\t\t\t}\n\t\t},\n\t\tLogger: l,\n\t}\n\n\t// start an http server with the proxy handler\n\t// which captures some parameters from the request\n\tvar remoteAddr string\n\tproxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tremoteAddr = r.RemoteAddr\n\t\tproxyHandler.ServeHTTP(w, r)\n\t}))\n\tdefer proxy.Close()\n\n\t// create the request\n\treq, _ := http.NewRequest(\"GET\", proxy.URL+\"/foo?x=y\", nil)\n\treq.Host = \"example.com\"\n\treq.Header.Set(\"X-Foo\", \"bar\")\n\n\t// execute the request\n\tresp, _ := mustDo(req)\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(\"http.Get: want 200 got \", resp.StatusCode)\n\t}\n\n\tupstreamURL, _ := url.Parse(server.URL)\n\tupstreamHost, upstreamPort, _ := net.SplitHostPort(upstreamURL.Host)\n\tremoteHost, remotePort, _ := net.SplitHostPort(remoteAddr)\n\twant := []string{\n\t\t\"header.X-Foo:bar\",\n\t\t\"remote_addr:\" + remoteAddr,\n\t\t\"remote_host:\" + remoteHost,\n\t\t\"remote_port:\" + remotePort,\n\t\t\"request:GET /foo?x=y HTTP/1.1\",\n\t\t\"request_args:x=y\",\n\t\t\"request_host:example.com\",\n\t\t\"request_method:GET\",\n\t\t\"request_proto:HTTP/1.1\",\n\t\t\"request_scheme:http\",\n\t\t\"request_uri:/foo?x=y\",\n\t\t\"request_url:http://example.com/foo?x=y\",\n\t\t\"response_body_size:\" + strconv.Itoa(bodySize),\n\t\t\"response_status:200\",\n\t\t\"response_time_ms:1.111\",\n\t\t\"response_time_ns:1.111111111\",\n\t\t\"response_time_us:1.111111\",\n\t\t\"time_common:01/Jan/2016:00:00:01 +0000\",\n\t\t\"time_rfc3339:2016-01-01T00:00:01Z\",\n\t\t\"time_rfc3339_ms:2016-01-01T00:00:01.123Z\",\n\t\t\"time_rfc3339_ns:2016-01-01T00:00:01.123456789Z\",\n\t\t\"time_rfc3339_us:2016-01-01T00:00:01.123456Z\",\n\t\t\"time_unix_ms:1451606401123\",\n\t\t\"time_unix_ns:1451606401123456789\",\n\t\t\"time_unix_us:1451606401123456\",\n\t\t\"upstream_addr:\" + upstreamURL.Host,\n\t\t\"upstream_host:\" + upstreamHost,\n\t\t\"upstream_port:\" + upstreamPort,\n\t\t\"upstream_request_scheme:\" + upstreamURL.Scheme,\n\t\t\"upstream_request_uri:/foo?x=y\",\n\t\t\"upstream_request_url:\" + upstreamURL.String() + \"/foo?x=y\",\n\t\t\"upstream_service:svc-a\",\n\t}\n\n\tdata := b.String()\n\tdata = data[:len(data)-1] // strip \\n\n\tgot := strings.Split(data, \";\")\n\tsort.Strings(got)\n\n\tverify.Values(t, \"\", got, want)\n}\n\nfunc TestProxyHTTPSUpstream(t *testing.T) {\n\tserver := httptest.NewUnstartedServer(okHandler)\n\tserver.TLS = tlsServerConfig()\n\tserver.StartTLS()\n\tdefer server.Close()\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{},\n\t\tTransport: &http.Transport{TLSClientConfig: tlsClientConfig()},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv / \" + server.URL + ` opts \"proto=https\"`))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tresp, body := mustGet(proxy.URL)\n\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t}\n\tif got, want := string(body), \"OK\"; got != want {\n\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t}\n}\n\ntype sniHandler struct {\n\tsni string\n}\n\nfunc (s *sniHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {\n\tif request.TLS != nil {\n\t\ts.sni = request.TLS.ServerName\n\t}\n\twriter.Write([]byte(`OK`))\n}\n\nfunc TestProxyHTTPSTransport(t *testing.T) {\n\tsni := &sniHandler{}\n\n\tserver := httptest.NewUnstartedServer(sni)\n\tserver.TLS = tlsServerConfig()\n\tserver.StartTLS()\n\tdefer server.Close()\n\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{},\n\t\tTransport: &http.Transport{TLSClientConfig: tlsClientConfig()},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv / \" + server.URL + ` opts \"proto=https host=foo.com tlsskipverify=true\"`))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tresp, body := mustGet(proxy.URL)\n\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t}\n\tif got, want := string(body), \"OK\"; got != want {\n\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t}\n\tif got, want := sni.sni, \"foo.com\"; got != want {\n\t\tt.Fatalf(\"got sni %q want %q\", got, want)\n\t}\n\n}\n\nfunc TestProxyHTTPSUpstreamSkipVerify(t *testing.T) {\n\tserver := httptest.NewUnstartedServer(okHandler)\n\tserver.TLS = &tls.Config{}\n\tserver.StartTLS()\n\tdefer server.Close()\n\tproxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:    config.Proxy{},\n\t\tTransport: http.DefaultTransport,\n\t\tInsecureTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv / \" + server.URL + ` opts \"proto=https tlsskipverify=true\"`))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer proxy.Close()\n\n\tresp, body := mustGet(proxy.URL)\n\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\tt.Fatalf(\"got status %d want %d\", got, want)\n\t}\n\tif got, want := string(body), \"OK\"; got != want {\n\t\tt.Fatalf(\"got body %q want %q\", got, want)\n\t}\n}\n\nfunc TestProxyGzipHandler(t *testing.T) {\n\ttests := []struct {\n\t\tdesc            string\n\t\tcontent         http.HandlerFunc\n\t\tacceptEncoding  string\n\t\tcontentEncoding string\n\t\twantResponse    []byte\n\t}{\n\t\t{\n\t\t\tdesc:            \"plain body - compressed response\",\n\t\t\tcontent:         plainHandler(\"text/plain\"),\n\t\t\tacceptEncoding:  \"gzip\",\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\twantResponse:    gzipContent,\n\t\t},\n\t\t{\n\t\t\tdesc:            \"plain body - compressed response (with charset)\",\n\t\t\tcontent:         plainHandler(\"text/plain; charset=UTF-8\"),\n\t\t\tacceptEncoding:  \"gzip\",\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\twantResponse:    gzipContent,\n\t\t},\n\t\t{\n\t\t\tdesc:            \"compressed body - compressed response\",\n\t\t\tcontent:         gzipHandler(\"text/plain; charset=UTF-8\"),\n\t\t\tacceptEncoding:  \"gzip\",\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\twantResponse:    gzipContent,\n\t\t},\n\t\t{\n\t\t\tdesc:            \"plain body - plain response\",\n\t\t\tcontent:         plainHandler(\"text/plain\"),\n\t\t\tacceptEncoding:  \"\",\n\t\t\tcontentEncoding: \"\",\n\t\t\twantResponse:    plainContent,\n\t\t},\n\t\t{\n\t\t\tdesc:            \"compressed body - plain response\",\n\t\t\tcontent:         gzipHandler(\"text/plain\"),\n\t\t\tacceptEncoding:  \"\",\n\t\t\tcontentEncoding: \"\",\n\t\t\twantResponse:    plainContent,\n\t\t},\n\t\t{\n\t\t\tdesc:            \"plain body - plain response (no match)\",\n\t\t\tcontent:         plainHandler(\"text/javascript\"),\n\t\t\tacceptEncoding:  \"gzip\",\n\t\t\tcontentEncoding: \"\",\n\t\t\twantResponse:    plainContent,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt // capture loop var\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tserver := httptest.NewServer(tt.content)\n\t\t\tdefer server.Close()\n\n\t\t\tproxy := httptest.NewServer(&HTTPProxy{\n\t\t\t\tConfig: config.Proxy{\n\t\t\t\t\tGZIPContentTypes: regexp.MustCompile(\"^text/plain(;.*)?$\"),\n\t\t\t\t},\n\t\t\t\tTransport: http.DefaultTransport,\n\t\t\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\t\t\treturn &route.Target{URL: mustParse(server.URL)}\n\t\t\t\t},\n\t\t\t})\n\t\t\tdefer proxy.Close()\n\n\t\t\treq, _ := http.NewRequest(\"GET\", proxy.URL, nil)\n\t\t\treq.Header.Set(\"Accept-Encoding\", tt.acceptEncoding)\n\t\t\tresp, body := mustDo(req)\n\t\t\tif got, want := resp.StatusCode, http.StatusOK; got != want {\n\t\t\t\tt.Fatalf(\"got code %d want %d\", got, want)\n\t\t\t}\n\t\t\tif got, want := resp.Header.Get(\"Content-Encoding\"), tt.contentEncoding; got != want {\n\t\t\t\tt.Errorf(\"got content-encoding %q want %q\", got, want)\n\t\t\t}\n\t\t\tif got, want := body, tt.wantResponse; !bytes.Equal(got, want) {\n\t\t\t\tt.Errorf(\"got body %q want %q\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar plainContent = []byte(\"Hello World\")\nvar gzipContent = compress(plainContent)\n\nfunc plainHandler(contentType string) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", contentType)\n\t\tw.Write(plainContent)\n\t}\n}\n\nfunc gzipHandler(contentType string) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", contentType)\n\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\tw.Write(gzipContent)\n\t}\n}\n\nvar okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\tw.Write([]byte(\"OK\"))\n})\n\nfunc tlsInsecureConfig() *tls.Config {\n\treturn &tls.Config{InsecureSkipVerify: true}\n}\n\nfunc tlsClientConfig() *tls.Config {\n\trootCAs := x509.NewCertPool()\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {\n\t\tpanic(\"could not parse cert\")\n\t}\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert2); !ok {\n\t\tpanic(\"could not parse cert\")\n\t}\n\treturn &tls.Config{RootCAs: rootCAs}\n}\n\nfunc tlsServerConfig() *tls.Config {\n\tcert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)\n\tif err != nil {\n\t\tpanic(\"failed to set cert\")\n\t}\n\treturn &tls.Config{Certificates: []tls.Certificate{cert}}\n}\n\nfunc tlsServerConfig2() *tls.Config {\n\tcert, err := tls.X509KeyPair(internal.LocalhostCert2, internal.LocalhostKey2)\n\tif err != nil {\n\t\tpanic(\"failed to set cert\")\n\t}\n\treturn &tls.Config{Certificates: []tls.Certificate{cert}}\n}\n\nfunc mustParse(rawurl string) *url.URL {\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n\nfunc mustDo(req *http.Request) (*http.Response, []byte) {\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn resp, body\n}\n\nfunc mustGet(urlstr string) (*http.Response, []byte) {\n\treq, err := http.NewRequest(\"GET\", urlstr, nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn mustDo(req)\n}\n\n// compress returns the gzip compressed content of b.\nfunc compress(b []byte) []byte {\n\tvar buf bytes.Buffer\n\tw := gzip.NewWriter(&buf)\n\tif _, err := w.Write(b); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn buf.Bytes()\n}\n\nfunc BenchmarkProxyLogger(b *testing.B) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))\n\tdefer server.Close()\n\n\tformat := \"remote_addr time request body_bytes_sent http_referer http_user_agent server_name proxy_endpoint response_time request_args \"\n\tl, err := logger.New(os.Stdout, format)\n\tif err != nil {\n\t\tb.Fatal(\"logger.NewHTTPLogger:\", err)\n\t}\n\n\tproxy := &HTTPProxy{\n\t\tConfig: config.Proxy{\n\t\t\tLocalIP:        \"1.1.1.1\",\n\t\t\tClientIPHeader: \"X-Forwarded-For\",\n\t\t},\n\t\tTransport: http.DefaultTransport,\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add mock / \" + server.URL))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t\tLogger: l,\n\t}\n\n\treq := &http.Request{\n\t\tRequestURI: \"/\",\n\t\tHeader:     http.Header{\"X-Forwarded-For\": {\"1.2.3.4\"}},\n\t\tRemoteAddr: \"2.2.2.2:666\",\n\t\tURL:        &url.URL{},\n\t\tMethod:     \"GET\",\n\t\tProto:      \"HTTP/1.1\",\n\t}\n\n\tfor range b.N {\n\t\tproxy.ServeHTTP(httptest.NewRecorder(), req)\n\t}\n}\n"
  },
  {
    "path": "proxy/http_proxy.go",
    "content": "package proxy\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"errors\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/auth\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/logger\"\n\t\"github.com/fabiolb/fabio/noroute\"\n\t\"github.com/fabiolb/fabio/proxy/gzip\"\n\t\"github.com/fabiolb/fabio/route\"\n\t\"github.com/fabiolb/fabio/uuid\"\n)\n\ntype HttpStatsHandler struct {\n\t// Requests is a histogram metric which is updated for every request.\n\tRequests gkm.Histogram\n\n\t// Noroute is a counter metric which is updated for every request\n\t// where Lookup() returns nil.\n\tNoroute gkm.Counter\n\n\t// WSConn counts the number of open web socket connections.\n\tWSConn gkm.Gauge\n\n\t// StatusTimer is a histogram for given status codes\n\tStatusTimer gkm.Histogram\n\n\t// RedirectCounter - counts redirects\n\tRedirectCounter gkm.Counter\n}\n\n// HTTPProxy is a dynamic reverse proxy for HTTP and HTTPS protocols.\ntype HTTPProxy struct {\n\n\t// stats contains all of the stats bits\n\tStats HttpStatsHandler\n\n\t// Transport is the http connection pool configured with timeouts.\n\t// The proxy will panic if this value is nil.\n\tTransport http.RoundTripper\n\n\t// InsecureTransport is the http connection pool configured with\n\t// InsecureSkipVerify set. This is used for https proxies with\n\t// self-signed certs.\n\tInsecureTransport http.RoundTripper\n\n\t// Time returns the current time as the number of seconds since the epoch.\n\t// If Time is nil, time.Now is used.\n\tTime func() time.Time\n\n\t// Lookup returns a target host for the given request.\n\t// The proxy will panic if this value is nil.\n\tLookup func(*http.Request) *route.Target\n\n\t// Logger is the access logger for the requests.\n\tLogger logger.Logger\n\n\t// UUID returns a unique id in uuid format.\n\t// If UUID is nil, uuid.NewUUID() is used.\n\tUUID func() string\n\n\t// Auth schemes registered with the server\n\tAuthSchemes map[string]auth.AuthScheme\n\n\t// Config is the proxy configuration as provided during startup.\n\tConfig config.Proxy\n}\n\nfunc (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif p.Lookup == nil {\n\t\tpanic(\"no lookup function\")\n\t}\n\n\tif p.Config.RequestID != \"\" {\n\t\tid := p.UUID\n\t\tif id == nil {\n\t\t\tid = uuid.NewUUID\n\t\t}\n\t\tr.Header.Set(p.Config.RequestID, id())\n\t}\n\n\tt := p.Lookup(r)\n\n\tif t == nil {\n\t\tstatus := p.Config.NoRouteStatus\n\t\tif status < 100 || status > 999 {\n\t\t\tstatus = http.StatusNotFound\n\t\t}\n\t\tw.WriteHeader(status)\n\t\thtml := noroute.GetHTML()\n\t\tif html != \"\" {\n\t\t\tio.WriteString(w, html)\n\t\t}\n\t\treturn\n\t}\n\n\tif t.AccessDeniedHTTP(r) {\n\t\thttp.Error(w, \"access denied\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\tif !t.Authorized(r, w, p.AuthSchemes) {\n\t\thttp.Error(w, \"authorization failed\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\t// build the request url since r.URL will get modified\n\t// by the reverse proxy and contains only the RequestURI anyway\n\trequestURL := &url.URL{\n\t\tScheme:   scheme(r),\n\t\tHost:     r.Host,\n\t\tPath:     r.URL.Path,\n\t\tRawQuery: r.URL.RawQuery,\n\t}\n\n\tif t.RedirectCode != 0 && t.RedirectURL != nil {\n\t\thttp.Redirect(w, r, t.RedirectURL.String(), t.RedirectCode)\n\t\tif p.Stats.RedirectCounter != nil {\n\t\t\tp.Stats.RedirectCounter.With(\"code\", strconv.Itoa(t.RedirectCode)).Add(1)\n\t\t}\n\t\treturn\n\t}\n\n\t// build the real target url that is passed to the proxy\n\ttargetURL := &url.URL{\n\t\tScheme: t.URL.Scheme,\n\t\tHost:   t.URL.Host,\n\t\tPath:   r.URL.Path,\n\t}\n\tif t.URL.RawQuery == \"\" || r.URL.RawQuery == \"\" {\n\t\ttargetURL.RawQuery = t.URL.RawQuery + r.URL.RawQuery\n\t} else {\n\t\ttargetURL.RawQuery = t.URL.RawQuery + \"&\" + r.URL.RawQuery\n\t}\n\n\tif t.Host == \"dst\" {\n\t\tr.Host = targetURL.Host\n\t} else if t.Host != \"\" {\n\t\tr.Host = t.Host\n\t}\n\n\t// TODO(fs): The HasPrefix check seems redundant since the lookup function should\n\t// TODO(fs): have found the target based on the prefix but there may be other\n\t// TODO(fs): matchers which may have different rules. I'll keep this for\n\t// TODO(fs): a defensive approach.\n\tif t.StripPath != \"\" && strings.HasPrefix(r.URL.Path, t.StripPath) {\n\t\ttargetURL.Path = targetURL.Path[len(t.StripPath):]\n\t\t// ensure absolute path after stripping to maintain compliance with\n\t\t// section 5.3 of RFC7230 (https://tools.ietf.org/html/rfc7230#section-5.3)\n\t\tif !strings.HasPrefix(targetURL.Path, \"/\") {\n\t\t\ttargetURL.Path = \"/\" + targetURL.Path\n\t\t}\n\t}\n\n\tif t.PrependPath != \"\" {\n\t\ttargetURL.Path = t.PrependPath + targetURL.Path\n\t\t// ensure absolute path after stripping to maintain compliance with\n\t\t// section 5.3 of RFC7230 (https://tools.ietf.org/html/rfc7230#section-5.3)\n\t\tif !strings.HasPrefix(targetURL.Path, \"/\") {\n\t\t\ttargetURL.Path = \"/\" + targetURL.Path\n\t\t}\n\t}\n\n\tif err := addHeaders(r, p.Config, t.StripPath); err != nil {\n\t\thttp.Error(w, \"cannot parse \"+r.RemoteAddr, http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\taddResponseHeaders(w, r, p.Config)\n\n\tupgrade, accept := r.Header.Get(\"Upgrade\"), r.Header.Get(\"Accept\")\n\n\ttr := p.Transport\n\tif t.Transport != nil {\n\t\ttr = t.Transport\n\t} else if t.TLSSkipVerify {\n\t\ttr = p.InsecureTransport\n\t}\n\n\tvar h http.Handler\n\tswitch {\n\tcase upgrade == \"websocket\" || upgrade == \"Websocket\":\n\t\tr.URL = targetURL\n\t\tif targetURL.Scheme == \"https\" || targetURL.Scheme == \"wss\" {\n\t\t\th = newWSHandler(targetURL.Host, func(network, address string) (net.Conn, error) {\n\t\t\t\treturn tls.Dial(network, address, tr.(*http.Transport).TLSClientConfig)\n\t\t\t}, p.Stats.WSConn)\n\t\t} else {\n\t\t\th = newWSHandler(targetURL.Host, net.Dial, p.Stats.WSConn)\n\t\t}\n\n\tcase accept == \"text/event-stream\":\n\t\t// use the flush interval for SSE (server-sent events)\n\t\t// must be > 0s to be effective\n\t\th = newHTTPProxy(targetURL, tr, p.Config.FlushInterval)\n\n\tdefault:\n\t\th = newHTTPProxy(targetURL, tr, p.Config.GlobalFlushInterval)\n\t}\n\n\tif p.Config.GZIPContentTypes != nil {\n\t\th = gzip.NewGzipHandler(h, p.Config.GZIPContentTypes)\n\t}\n\n\ttimeNow := p.Time\n\tif timeNow == nil {\n\t\ttimeNow = time.Now\n\t}\n\n\tstart := timeNow()\n\trw := &responseWriter{w: w}\n\th.ServeHTTP(rw, r)\n\tend := timeNow()\n\tdur := end.Sub(start)\n\n\tif p.Stats.Requests != nil {\n\t\tp.Stats.Requests.Observe(dur.Seconds())\n\t}\n\tif t.Timer != nil {\n\t\tt.Timer.Observe(dur.Seconds())\n\t}\n\tif rw.code <= 0 {\n\t\treturn\n\t}\n\n\tif p.Stats.StatusTimer != nil {\n\t\tp.Stats.StatusTimer.With(\"code\", strconv.Itoa(rw.code)).Observe(dur.Seconds())\n\t}\n\n\t// write access log\n\tif p.Logger != nil {\n\t\tp.Logger.Log(&logger.Event{\n\t\t\tStart:   start,\n\t\t\tEnd:     end,\n\t\t\tRequest: r,\n\t\t\tResponse: &http.Response{\n\t\t\t\tStatusCode:    rw.code,\n\t\t\t\tContentLength: int64(rw.size),\n\t\t\t},\n\t\t\tRequestURL:      requestURL,\n\t\t\tUpstreamAddr:    targetURL.Host,\n\t\t\tUpstreamService: t.Service,\n\t\t\tUpstreamURL:     targetURL,\n\t\t})\n\t}\n}\n\n// responseWriter wraps an http.ResponseWriter to capture the status code and\n// the size of the response. It also implements http.Hijacker to forward\n// hijacking the connection to the wrapped writer if supported.\ntype responseWriter struct {\n\tw    http.ResponseWriter\n\tcode int\n\tsize int\n}\n\nfunc (rw *responseWriter) Header() http.Header {\n\treturn rw.w.Header()\n}\n\nfunc (rw *responseWriter) Write(b []byte) (int, error) {\n\tn, err := rw.w.Write(b)\n\trw.size += n\n\treturn n, err\n}\n\nfunc (rw *responseWriter) WriteHeader(statusCode int) {\n\trw.w.WriteHeader(statusCode)\n\trw.code = statusCode\n}\n\nfunc (rw *responseWriter) Flush() {\n\tif fl, ok := rw.w.(http.Flusher); ok {\n\t\tfl.Flush()\n\t}\n}\n\nvar errNoHijacker = errors.New(\"not a hijacker\")\n\nfunc (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\tif hj, ok := rw.w.(http.Hijacker); ok {\n\t\treturn hj.Hijack()\n\t}\n\treturn nil, nil, errNoHijacker\n}\n"
  },
  {
    "path": "proxy/inetaf_tcpproxy.go",
    "content": "package proxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/inetaf/tcpproxy\"\n)\n\ntype childProxy struct {\n\tl net.Listener\n\ts Server\n}\n\ntype InetAfTCPProxyServer struct {\n\tProxy    *tcpproxy.Proxy\n\tchildren []*childProxy\n}\n\n// Close - implements Server - is this even called?\nfunc (tps *InetAfTCPProxyServer) Close() error {\n\t_ = tps.Proxy.Close()\n\tfirstErr := tps.Proxy.Wait()\n\terrChan := make(chan error, len(tps.children))\n\tfor _, sl := range tps.children {\n\t\tgo func(sl *childProxy) {\n\t\t\terrChan <- sl.s.Close()\n\t\t}(sl)\n\t}\n\tfor range tps.children {\n\t\terr := <-errChan\n\t\tif errors.Is(err, http.ErrServerClosed) {\n\t\t\terr = nil\n\t\t}\n\t\tif firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] %s\", err)\n\t\t}\n\t}\n\treturn firstErr\n\n}\n\n// Serve - implements server.  The listener is ignored, but it\n// calls serve on the children\nfunc (tps *InetAfTCPProxyServer) Serve(_ net.Listener) error {\n\tif len(tps.children) == 0 {\n\t\treturn fmt.Errorf(\"no children defined for listener\")\n\t}\n\terrChan := make(chan error, len(tps.children))\n\tfor _, sl := range tps.children {\n\t\tgo func(sl *childProxy) {\n\t\t\terrChan <- sl.s.Serve(sl.l)\n\t\t}(sl)\n\t}\n\tfirstErr := tps.Proxy.Wait()\n\tfor range tps.children {\n\t\terr := <-errChan\n\t\tif errors.Is(err, http.ErrServerClosed) {\n\t\t\terr = nil\n\t\t}\n\t\tif firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Print(\"[FATAL] \", err)\n\t\t}\n\t}\n\treturn firstErr\n}\n\n// ServeLater - l is really only for listeners that are\n// tcpproxy.TargetListener or a derivative.  Don't call after\n// Serve() is called.\nfunc (tps *InetAfTCPProxyServer) ServeLater(l net.Listener, s Server) {\n\ttps.children = append(tps.children, &childProxy{l, s})\n}\n\nfunc (tps *InetAfTCPProxyServer) Shutdown(ctx context.Context) error {\n\t_ = tps.Proxy.Close()        // always returns nil error anyway\n\tfirstErr := tps.Proxy.Wait() // wait for outer listener to close before telling the childProxy\n\terrChan := make(chan error, len(tps.children))\n\tfor _, sl := range tps.children {\n\t\tgo func(sl *childProxy) {\n\t\t\terrChan <- sl.s.Shutdown(ctx)\n\t\t}(sl)\n\t}\n\tfor range tps.children {\n\t\terr := <-errChan\n\t\tif firstErr == nil {\n\t\t\tfirstErr = err\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Print(\"[ERROR] \", err)\n\t\t}\n\t}\n\treturn firstErr\n}\n"
  },
  {
    "path": "proxy/inetaf_tcpproxy_integration_test.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/proxy/tcp\"\n\t\"github.com/fabiolb/fabio/proxy/tcp/tcptest\"\n\t\"github.com/fabiolb/fabio/route\"\n)\n\n// to run this test, add the following to /etc/hosts:\n// 127.0.0.1\texample.com\n// 127.0.0.1\texample2.com\n// and then set the environment FABIO_IHAVEHOSTENTRIES=true\n// This also runs in Github Actions by default, since the workflow adds these aliases.\nfunc TestProxyTCPAndHTTPS(t *testing.T) {\n\tif os.Getenv(\"TRAVIS\") != \"true\" &&\n\t\tos.Getenv(\"CI\") != \"true\" &&\n\t\tos.Getenv(\"FABIO_IHAVEHOSTENTRIES\") != \"true\" {\n\t\tt.Skip(\"skipping because env FABIO_IHAVEHOSTENTRIES is not set to true\")\n\t}\n\n\ttlsCfg1 := tlsServerConfig()\n\ttlsCfg2 := tlsServerConfig2()\n\ttcpServer := httptest.NewUnstartedServer(okHandler)\n\ttcpServer.TLS = tlsCfg2\n\ttcpServer.StartTLS()\n\tdefer tcpServer.Close()\n\n\thttpPayload := []byte(`OK HTTP`)\n\n\thttpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write(httpPayload)\n\t}))\n\tdefer httpServer.Close()\n\n\ttpl := `route add srv / %s opts \"proto=https\"\nroute add tcproute example2.com/ tcp://%s opts \"proto=tcp\"`\n\n\ttable, _ := route.NewTable(bytes.NewBufferString(fmt.Sprintf(tpl, httpServer.URL, tcpServer.Listener.Addr())))\n\thp := &HTTPProxy{\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\treturn table.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t}\n\n\ttp := &tcp.SNIProxy{\n\t\tLookup: func(h string) *route.Target {\n\t\t\treturn table.LookupHost(h, route.Picker[\"rr\"])\n\t\t},\n\t}\n\tm := func(_ context.Context, h string) bool {\n\t\t// TODO - matcher needs to move out of main\n\t\t// so we can test it more easily.  Probably\n\t\t// the other functions too.\n\t\tt := table.LookupHost(h, route.Picker[\"rr\"])\n\t\tif t == nil {\n\t\t\treturn false\n\t\t}\n\t\t// Make sure this is supposed to be a tcp proxy.\n\t\t// opts proto= overrides scheme if present.\n\t\tvar (\n\t\t\tok    bool\n\t\t\tproto string\n\t\t)\n\t\tif proto, ok = t.Opts[\"proto\"]; !ok && t.URL != nil {\n\t\t\tproto = t.URL.Scheme\n\t\t}\n\t\treturn proto == \"tcp\"\n\t}\n\n\t// get an unused port for use for the proxy.  the rest of the tests just\n\t// pick a high-numbered port, but this should be safer, if ugly.  could\n\t// also just fire up a listener with 0 as the port and let the stack\n\t// pick one - which is what httptest does - but this is less lines and\n\t// I'm lazy. --NJ\n\ttmp := httptest.NewServer(okHandler)\n\tproxyAddr := tmp.Listener.Addr().String()\n\ttmp.Close()\n\t_, port, err := net.SplitHostPort(proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"error determining port from addr: %s\", err)\n\t}\n\n\tl := config.Listen{Addr: proxyAddr}\n\tgo func() {\n\t\terr := ListenAndServeHTTPSTCPSNI(l, hp, tp, tlsCfg1, m)\n\t\tif err != nil {\n\t\t\tt.Logf(\"error shutting down: %s\", err)\n\t\t}\n\t}()\n\tdefer Close()\n\t// retry until listener is responding.\n\td, err := tcptest.NewRetryDialer().Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"error connecting to proxy: %s\", err)\n\t}\n\td.Close()\n\t// At this point, the proxy should up and listening and will do\n\t// tcp proxy to https://example2.com, and terminate TLS for\n\t// https://example.com\n\n\tc := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig:   tlsClientConfig(),\n\t\t\tDisableKeepAlives: true,\n\t\t\tMaxConnsPerHost:   -1,\n\t\t},\n\t}\n\n\t// make sure tcp steering happens for https://example2.com/\n\t// and https proxying happens for https://example.com/\n\tfor _, data := range []struct {\n\t\tname string\n\t\tu    string\n\t\th    string\n\t\tbody []byte\n\t}{{\n\t\tname: \"https proxy for example.com\",\n\t\tu:    \"https://example.com:\" + port,\n\t\th:    \"example.com\",\n\t\tbody: httpPayload,\n\t}, {\n\t\tname: \"tcp proxy for example2.com serving https\",\n\t\tu:    \"https://example2.com:\" + port,\n\t\th:    \"example2.com\",\n\t\tbody: []byte(`OK`),\n\t}} {\n\t\tt.Run(data.name, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(http.MethodGet, data.u, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error creating req: %s\", err)\n\t\t\t}\n\t\t\tresp, err := c.Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error on request %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error reading body: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !bytes.Equal(body, data.body) {\n\t\t\t\tt.Error(\"http body not equal\")\n\t\t\t}\n\t\t\tif len(resp.TLS.PeerCertificates) != 1 {\n\t\t\t\tt.Errorf(\"unexpected peer certs\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !foundDNSName(resp.TLS.PeerCertificates[0], data.h) {\n\t\t\t\tt.Error(\"wrong certificate returned\")\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc foundDNSName(crt *x509.Certificate, dnsName string) bool {\n\tfound := false\n\tfor _, dname := range crt.DNSNames {\n\t\tif dname == dnsName {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\treturn found\n}\n"
  },
  {
    "path": "proxy/internal/testcert.go",
    "content": "package internal\n\n// LocalhostCert is a PEM-encoded TLS cert with SAN IPs\n// \"127.0.0.1\" and \"[::1]\", expiring at Jan 29 16:00:00 2084 GMT.\n// generated from src/crypto/tls:\n// go run generate_cert.go  --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date \"Jan 1 00:00:00 1970\" --duration=1000000h\nvar LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----\nMIICEzCCAXygAwIBAgIQS3cofn+2H4NxFntgaMRAPTANBgkqhkiG9w0BAQsFADAS\nMRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw\nMDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB\niQKBgQDp+sQVBNYwZ4YSskddAtTYq2NPdWYawNw9YQDBU9ft3fIm1r9UoyL/57bo\ngCgFAkglXo06sAfuk+W6OXRPplEwxCU/mAiAjMLKES1V3oZnI42sTeiskdvb8j6E\n47EpbWSA2OU4Nqulbh6vkGrzYzUdlmwwz+rGvfmHp1EOjMVzvQIDAQABo2gwZjAO\nBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw\nAwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA\nAAAAATANBgkqhkiG9w0BAQsFAAOBgQBChdgkaHaw83GFx8aDWoE3K4+h9YqXuvEP\nb2OWAYlzY/U99BA9P0lE4vGpaIAeCFxalJ2AK3yHjt+eezy3sw0bMeG8ZNYcOyIV\nexS95UdAKFt93a5zIWrkYQvhuzln1IOxPJQZ4rkq4nikLj2WuyGR7QnuVBdgPqP7\nRN4BPb5Sog==\n-----END CERTIFICATE-----`)\n\n// LocalhostKey is the private key for LocalhostCert.\nvar LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQDp+sQVBNYwZ4YSskddAtTYq2NPdWYawNw9YQDBU9ft3fIm1r9U\noyL/57bogCgFAkglXo06sAfuk+W6OXRPplEwxCU/mAiAjMLKES1V3oZnI42sTeis\nkdvb8j6E47EpbWSA2OU4Nqulbh6vkGrzYzUdlmwwz+rGvfmHp1EOjMVzvQIDAQAB\nAoGAGVoXduOQRaxh5ZK1kslkwJlJaGmjB5EQDAJ/r3LjOZ3LyBOKpaQLfcjgk66X\nJ3vIz2vAR7SdF2elA5mIFb1CnJ4HW4cWHzgFQdUnUtoUNuMPy/9QREFfeag9GMPx\ndZNiypiKqHDSY5ovUL92gtv5W0/w00lYpFiBaYLl+WHvQ6ECQQDvZpULZCEmZHwL\nhZun4ObzLwFNZ9sNPgwJybnxVYaolXACeh4Ewur0kZlY9DJMqo7Rz82JWuFarkgU\nGQK/L231AkEA+jP0+q7jfI8NJqwpWFDjwKiI7fadClcdUgXvW2c5wc2pEe4KiAqs\nZOWPGsH7SxigGRLzw01SCoInX5yw689JqQJARIOTPENXyWkQpyuBtLYE4qwdL039\nvvh28YYuFQdpFm5ONCdG2A4AuCXDQVYB3zcg0KMsK5c6z3z5W+cchiLI0QJBAIDS\nZYz4pNoKEVxbAgKdy1XzsGTNN/gN+GO1+JJYKK23RRidNkDrNe3RIAhH3inBKRUf\n4/AnjFkqwDkDRTh0htkCQQDfrRZr+gazwzDTSp23+l6MEbqBbc+TTC3c40zpNj4a\negxjd5+SkMj6zXEJxAOgo+LmQDGWsu1YQ+XXL87VPwIP\n-----END RSA PRIVATE KEY-----`)\n\n// LocalhostCert2 is a PEM-encoded TLS cert with SAN IPs\n// \"127.0.0.1\" and \"[::1]\", expiring at Jan 29 16:00:00 2084 GMT.\n// generated from src/crypto/tls:\n// go run generate_cert.go  --rsa-bits 1024 --host 127.0.0.1,::1,example2.com --ca --start-date \"Jan 1 00:00:00 1970\" --duration=1000000h\nvar LocalhostCert2 = []byte(`-----BEGIN CERTIFICATE-----\nMIICNTCCAZ6gAwIBAgIQeD/ltjjdLHO9L2c5eMG5rDANBgkqhkiG9w0BAQsFADAS\nMRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw\nMDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB\niQKBgQDzwrR53c4RyXpitbeRD9CY6PRhqYgnrCOBy0GUuGs5hJgMqSMXuIH4Vs4h\nlOH19hb9o733O+qJM6s4D8GNfz2LC/SC/DOHqXv0DeB6lGJ47I2Wv8569uFjNh3K\npi5yYlAqNdjQ1TYjUZmDytiQxp8eCLCKGpbWvjWzop50GTefqwIDAQABo4GJMIGG\nMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8E\nBTADAQH/MB0GA1UdDgQWBBSLtvT+Rtv9N+Tw1ZbXU+jSxYqYxTAvBgNVHREEKDAm\nggxleGFtcGxlMi5jb22HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcN\nAQELBQADgYEAkO5LWq8SHx8dUgMDryCyrJamsFT3Z/Lt1zMfJfNdTRSvsg7Fy3XR\nIOtxPqh2gT7OsSeU6fjjbDUTuGmH/BckwZTFMkRho/WaEgbP6XWWjkl+6euJBvtG\nlBFElB/HVPa5puggihR9H1pE3s+SdtslwfOf8XsUA4xlcrhpU5kuMa4=\n-----END CERTIFICATE-----`)\n\n// LocalhostKey2 is the private key for LocalhostCert2.\nvar LocalhostKey2 = []byte(`-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAPPCtHndzhHJemK1\nt5EP0Jjo9GGpiCesI4HLQZS4azmEmAypIxe4gfhWziGU4fX2Fv2jvfc76okzqzgP\nwY1/PYsL9IL8M4epe/QN4HqUYnjsjZa/znr24WM2HcqmLnJiUCo12NDVNiNRmYPK\n2JDGnx4IsIoalta+NbOinnQZN5+rAgMBAAECgYEAlRnYuN5SiRC7WpuacBHDX3TG\n3uILFXE2utKwB58Sfzk6pCvk+kJyxYubRHFEEeX4RCcfMJYmrMu9BGqm0r0sz6nb\nCSZk2Crn7eKgLK01+t2K+2s1R2oNB/fxkmxVUTbxiZ+Bt7xsAvFnnQRl06r9NNYo\nXqAadfRFGlmMkSEAbbkCQQD9feneVqIdPPGQOadUq8VYY/M8onMSG72O0qr6Dx8X\nj3hXf5D91pvXos+h3TwSICQ354BcakI4VK/EXxfmNUSXAkEA9iwkdXYQXkjgqpwH\n3jMxG3DLAl9aHnykFSm8G2vQj2427ePnLppHclXKTPfu3E0qUNT2WgOK64N3qC1F\nNkZ8DQJBAPSOlKFfnVlt4XOOW8QRT/wduZ4G79NJlhCDaFaFXi7ByI1J0h1C/ekE\n9yInKXwnLCoPG0SNc0ObWFOwloMPYxMCQEIH5yOmro9Lxw+cWLPuUU7F+35Aa2Dg\nF/chQbatPb0rWAqJZhpnAaEWh/QLUQPAowgZh5bvelTf57mxou4DDAUCQHynSBDh\nGPDiBeTKX+VF50yHR8P6YrbeLIwasw19HA2BVhKKP05ZbQnWtN/ekhhBbI5fTwn0\nnjeVQ3HbrfnY1Ik=\n-----END PRIVATE KEY-----`)\n"
  },
  {
    "path": "proxy/listen.go",
    "content": "package proxy\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"net\"\n\t\"time\"\n\n\tproxyproto \"github.com/armon/go-proxyproto\"\n)\n\nfunc ListenTCP(l config.Listen, cfg *tls.Config) (net.Listener, error) {\n\taddr, err := net.ResolveTCPAddr(\"tcp\", l.Addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"listen: Fail to resolve tcp addr. %s\", l.Addr)\n\t}\n\n\tvar ln net.Listener\n\tln, err = net.ListenTCP(\"tcp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"listen: Fail to listen. %s\", err)\n\t}\n\n\t// enable TCPKeepAlive support\n\tln = tcpKeepAliveListener{ln.(*net.TCPListener)}\n\n\t// enable PROXY protocol support\n\tif l.ProxyProto {\n\t\tln = &proxyproto.Listener{\n\t\t\tListener:           ln,\n\t\t\tProxyHeaderTimeout: l.ProxyHeaderTimeout,\n\t\t}\n\t}\n\n\t// enable TLS\n\tif cfg != nil {\n\t\tln = tls.NewListener(ln, cfg)\n\t}\n\n\treturn &tcpListener{ln, addr, cfg}, nil\n}\n\ntype tcpListener struct {\n\tl         net.Listener\n\taddr      net.Addr\n\ttlsConfig *tls.Config\n}\n\nfunc (ln *tcpListener) Addr() net.Addr {\n\treturn ln.addr\n}\n\nfunc (ln *tcpListener) Accept() (net.Conn, error) {\n\treturn ln.l.Accept()\n}\n\nfunc (ln *tcpListener) Close() error {\n\treturn ln.l.Close()\n}\n\n// copied from http://golang.org/src/net/http/server.go?s=54604:54695#L1967\n// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted\n// connections. It's used by ListenAndServe and ListenAndServeTLS so\n// dead TCP connections (e.g. closing laptop mid-download) eventually\n// go away.\ntype tcpKeepAliveListener struct {\n\t*net.TCPListener\n}\n\nfunc (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {\n\ttc, err := ln.AcceptTCP()\n\tif err != nil {\n\t\treturn\n\t}\n\tif err = tc.SetKeepAlive(true); err != nil {\n\t\treturn\n\t}\n\tif err = tc.SetKeepAlivePeriod(3 * time.Minute); err != nil {\n\t\treturn\n\t}\n\treturn tc, nil\n}\n"
  },
  {
    "path": "proxy/listen_test.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/route\"\n)\n\nfunc TestGracefulShutdown(t *testing.T) {\n\n\t// start a server which responds after the shutdown has been triggered.\n\ttrigger := make(chan bool)\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t<-trigger\n\t}))\n\tdefer srv.Close()\n\n\t// start proxy\n\taddr := \"127.0.0.1:57777\"\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\th := &HTTPProxy{\n\t\t\tTransport: http.DefaultTransport,\n\t\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add svc / \" + srv.URL))\n\t\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: addr}\n\t\tif err := ListenAndServeHTTP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeHTTP: \", err)\n\t\t}\n\t}()\n\n\t// trigger shutdown after some time\n\tdelay := 100 * time.Millisecond\n\tgo func() {\n\t\ttime.Sleep(delay)\n\t\tclose(trigger)\n\t\tShutdown(delay)\n\t}()\n\n\t// give server some time to start up\n\ttime.Sleep(delay / 2)\n\n\tmakeReq := func() (int, error) {\n\t\tresp, err := http.Get(\"http://\" + addr + \"/\")\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\treturn resp.StatusCode, nil\n\t}\n\n\t// make 200 OK request\n\t// start before and complete after shutdown was triggered\n\tcode, err := makeReq()\n\tif err != nil {\n\t\tt.Fatalf(\"request 1: got error %q want nil\", err)\n\t}\n\tif got, want := code, 200; got != want {\n\t\tt.Fatalf(\"request 1: got %v want %v\", got, want)\n\t}\n\n\t// make request to closed server\n\t_, err = makeReq()\n\tif got, want := err.Error(), \"connection refused\"; !strings.Contains(got, want) {\n\t\tt.Fatalf(\"request 2: got error %q want %q\", got, want)\n\t}\n\n\t// wait for listen() to return\n\t// note that the actual listeners have not returned yet\n\twg.Wait()\n}\n"
  },
  {
    "path": "proxy/serve.go",
    "content": "package proxy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/proxy/tcp\"\n\n\t\"github.com/armon/go-proxyproto\"\n\t\"github.com/inetaf/tcpproxy\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\ntype Server interface {\n\tClose() error\n\tServe(l net.Listener) error\n\tShutdown(ctx context.Context) error\n}\n\nvar (\n\t// mu guards servers which contains the list\n\t// of running proxy servers.\n\tmu      sync.Mutex\n\tservers = make(map[string]Server)\n)\n\nfunc CloseProxy(address string) error {\n\tmu.Lock()\n\tif srv, ok := servers[address]; ok {\n\t\terr := srv.Close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Printf(\"[INFO] Dynamic TCP listener on %s has been terminated\", address)\n\t\tdelete(servers, address)\n\t}\n\tmu.Unlock()\n\treturn nil\n}\n\nfunc Close() {\n\tmu.Lock()\n\tfor _, srv := range servers {\n\t\tsrv.Close()\n\t}\n\tservers = make(map[string]Server)\n\tmu.Unlock()\n}\n\nfunc Shutdown(timeout time.Duration) {\n\tmu.Lock()\n\tsrvs := make(map[string]Server, len(servers))\n\tfor k, v := range servers {\n\t\tsrvs[k] = v\n\t}\n\tservers = make(map[string]Server)\n\tmu.Unlock()\n\n\tvar wg sync.WaitGroup\n\tfor _, srv := range srvs {\n\t\twg.Add(1)\n\t\tgo func(srv Server) {\n\t\t\tdefer wg.Done()\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\tdefer cancel()\n\t\t\tsrv.Shutdown(ctx)\n\t\t}(srv)\n\t}\n\twg.Wait()\n}\n\nfunc ListenAndServeHTTP(l config.Listen, h http.Handler, cfg *tls.Config) error {\n\tln, err := ListenTCP(l, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsrv := &http.Server{\n\t\tAddr:         l.Addr,\n\t\tHandler:      h,\n\t\tReadTimeout:  l.ReadTimeout,\n\t\tWriteTimeout: l.WriteTimeout,\n\t\tIdleTimeout:  l.IdleTimeout,\n\t\tTLSConfig:    cfg,\n\t}\n\treturn serve(ln, srv)\n}\n\nfunc ListenAndServePrometheus(l config.Listen, pcfg config.Prometheus, cfg *tls.Config) error {\n\tln, err := ListenTCP(l, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmux := http.NewServeMux()\n\tif pcfg.Path != \"/\" {\n\t\tmux.HandleFunc(\"/\", func(rw http.ResponseWriter, _ *http.Request) {\n\t\t\trw.Header().Set(\"Location\", pcfg.Path)\n\t\t\trw.WriteHeader(http.StatusPermanentRedirect)\n\t\t})\n\t}\n\tmux.Handle(pcfg.Path, promhttp.Handler())\n\n\tsrv := &http.Server{\n\t\tAddr:         l.Addr,\n\t\tHandler:      mux,\n\t\tReadTimeout:  l.ReadTimeout,\n\t\tWriteTimeout: l.WriteTimeout,\n\t\tIdleTimeout:  l.IdleTimeout,\n\t\tTLSConfig:    cfg,\n\t}\n\treturn serve(ln, srv)\n}\n\nfunc ListenAndServeHTTPSTCPSNI(l config.Listen, h http.Handler, p tcp.Handler, cfg *tls.Config, m tcpproxy.Matcher) error {\n\t// we only want proxy proto enabled on tcp proxies\n\tpxyProto := l.ProxyProto\n\tl.ProxyProto = false\n\ttp := &tcpproxy.Proxy{\n\t\tListenFunc: func(net, laddr string) (net.Listener, error) {\n\t\t\t// cfg is nil here so it's not terminating TLS (yet)\n\t\t\treturn ListenTCP(l, nil)\n\t\t},\n\t}\n\n\t// This inspects SNI for matches.  If this succeeds then we Proxy tcp.\n\ttcpSNIListener := &tcpproxy.TargetListener{Address: l.Addr}\n\ttp.AddSNIMatchRoute(l.Addr, m, tcpSNIListener)\n\n\t// Fallthrough to https\n\thttpsListener := &tcpproxy.TargetListener{Address: l.Addr}\n\ttp.AddRoute(l.Addr, httpsListener)\n\n\t// Start the listener\n\terr := tp.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttps := &InetAfTCPProxyServer{Proxy: tp}\n\tvar tln net.Listener = tcpSNIListener\n\t// enable proxy protocol on the tcp side if configured to do so\n\tif pxyProto {\n\t\ttln = &proxyproto.Listener{\n\t\t\tListener:           tln,\n\t\t\tProxyHeaderTimeout: l.ProxyHeaderTimeout,\n\t\t}\n\t}\n\ttps.ServeLater(tln, &tcp.Server{\n\t\tAddr:         l.Addr,\n\t\tHandler:      p,\n\t\tReadTimeout:  l.ReadTimeout,\n\t\tWriteTimeout: l.WriteTimeout,\n\t})\n\n\t// wrap TargetListener in a tls terminating version for HTTPS\n\ttps.ServeLater(tls.NewListener(httpsListener, cfg), &http.Server{\n\t\tAddr:         l.Addr,\n\t\tHandler:      h,\n\t\tReadTimeout:  l.ReadTimeout,\n\t\tWriteTimeout: l.WriteTimeout,\n\t\tIdleTimeout:  l.IdleTimeout,\n\t\tTLSConfig:    cfg,\n\t})\n\n\t// tcpproxy creates its own listener from the configuration above so we can\n\t// safely pass nil here, nonetheless we are passing `httpsListener` to\n\t// extract it's address and save server in the `servers` map.\n\treturn serve(httpsListener, tps)\n}\n\nfunc ListenAndServeGRPC(l config.Listen, opts []grpc.ServerOption, cfg *tls.Config) error {\n\tln, err := ListenTCP(l, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsrv := &gRPCServer{\n\t\tserver: grpc.NewServer(opts...),\n\t}\n\n\treturn serve(ln, srv)\n}\n\nfunc ListenAndServeTCP(l config.Listen, h tcp.Handler, cfg *tls.Config) error {\n\tln, err := ListenTCP(l, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsrv := &tcp.Server{\n\t\tAddr:         l.Addr,\n\t\tHandler:      h,\n\t\tReadTimeout:  l.ReadTimeout,\n\t\tWriteTimeout: l.WriteTimeout,\n\t}\n\treturn serve(ln, srv)\n}\n\nfunc serve(ln net.Listener, srv Server) error {\n\tmu.Lock()\n\tservers[ln.Addr().String()] = srv\n\tmu.Unlock()\n\terr := srv.Serve(ln)\n\tif err != nil {\n\t\tvar opErr *net.OpError\n\t\tif errors.Is(err, http.ErrServerClosed) {\n\t\t\terr = nil\n\t\t} else if errors.As(err, &opErr) {\n\t\t\tif opErr.Err != nil && opErr.Err.Error() == \"use of closed network connection\" {\n\t\t\t\terr = nil\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "proxy/tcp/copy_buffer.go",
    "content": "package tcp\n\nimport (\n\t\"io\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n)\n\n// copyBuffer is an adapted version of io.copyBuffer which updates a\n// counter instead of returning the total bytes written.\nfunc copyBuffer(dst io.Writer, src io.Reader, c gkm.Counter) (err error) {\n\tbuf := make([]byte, 32*1024)\n\tfor {\n\t\tnr, er := src.Read(buf)\n\t\tif nr > 0 {\n\t\t\tnw, ew := dst.Write(buf[0:nr])\n\t\t\tif nw > 0 {\n\t\t\t\tif c != nil {\n\t\t\t\t\tc.Add(float64(nw))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ew != nil {\n\t\t\t\terr = ew\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif nr != nw {\n\t\t\t\terr = io.ErrShortWrite\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif er != nil {\n\t\t\tif er != io.EOF {\n\t\t\t\terr = er\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "proxy/tcp/proxy_proto.go",
    "content": "package tcp\n\nimport (\n\t\"net\"\n)\n\n// WriteProxyHeader extracts remote and local IP address and port\n// combinations from incoming connection and writes the PROXY proto\n// header to the outgoing connection\nfunc WriteProxyHeader(out, in net.Conn) error {\n\tclientAddr, clientPort, _ := net.SplitHostPort(in.RemoteAddr().String())\n\tserverAddr, serverPort, _ := net.SplitHostPort(in.LocalAddr().String())\n\n\tvar proto string\n\tif net.ParseIP(clientAddr).To4() != nil {\n\t\tproto = \"TCP4\"\n\t} else {\n\t\tproto = \"TCP6\"\n\t}\n\n\theader := \"PROXY \" + proto + \" \" + clientAddr + \" \" + serverAddr + \" \" + clientPort + \" \" + serverPort + \"\\r\\n\"\n\t_, err := out.Write([]byte(header))\n\treturn err\n}\n"
  },
  {
    "path": "proxy/tcp/server.go",
    "content": "package tcp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Handler responds to a TCP request.\n//\n// ServeTCP should write responses to the in connection and close\n// it on return.\ntype Handler interface {\n\tServeTCP(in net.Conn) error\n}\n\ntype HandlerFunc func(in net.Conn) error\n\nfunc (f HandlerFunc) ServeTCP(in net.Conn) error {\n\treturn f(in)\n}\n\n// Server implements a generic TCP server.\ntype Server struct {\n\tHandler      Handler\n\tconns        map[net.Conn]bool\n\tAddr         string\n\tlisteners    []net.Listener\n\tReadTimeout  time.Duration\n\tWriteTimeout time.Duration\n\n\tmu sync.Mutex\n}\n\nfunc (s *Server) ListenAndServe() error {\n\tl, err := net.Listen(\"tcp\", s.Addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer l.Close()\n\treturn s.Serve(l)\n}\n\nfunc (s *Server) ListenAndServeTLS(certFile, keyFile string) error {\n\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg := &tls.Config{Certificates: []tls.Certificate{cert}}\n\tl, err := tls.Listen(\"tcp\", s.Addr, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer l.Close()\n\treturn s.Serve(l)\n}\n\nfunc (s *Server) Serve(l net.Listener) error {\n\tdefer l.Close()\n\n\ts.mu.Lock()\n\ts.listeners = append(s.listeners, l)\n\ts.mu.Unlock()\n\n\tfor {\n\t\tc, err := l.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc = &conn{\n\t\t\tc:            c,\n\t\t\tReadTimeout:  s.ReadTimeout,\n\t\t\tWriteTimeout: s.WriteTimeout,\n\t\t}\n\t\ts.mu.Lock()\n\t\tif s.conns == nil {\n\t\t\ts.conns = map[net.Conn]bool{}\n\t\t}\n\t\ts.conns[c] = true\n\t\ts.mu.Unlock()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tc.Close()\n\t\t\t\ts.mu.Lock()\n\t\t\t\tdelete(s.conns, c)\n\t\t\t\ts.mu.Unlock()\n\t\t\t}()\n\n\t\t\ts.Handler.ServeTCP(c)\n\t\t}()\n\t}\n}\n\nfunc (s *Server) closeListeners() {\n\ts.mu.Lock()\n\tfor _, l := range s.listeners {\n\t\tl.Close()\n\t}\n\ts.listeners = nil\n\ts.mu.Unlock()\n}\n\nfunc (s *Server) closeConns() error {\n\ts.mu.Lock()\n\tfor c := range s.conns {\n\t\tc.Close()\n\t}\n\ts.conns = nil\n\ts.mu.Unlock()\n\treturn nil\n}\n\nfunc (s *Server) Close() error {\n\ts.closeListeners()\n\treturn s.closeConns()\n}\n\nfunc (s *Server) Shutdown(ctx context.Context) error {\n\ts.closeListeners()\n\tif ctx != nil {\n\t\t<-ctx.Done()\n\t}\n\treturn s.closeConns()\n}\n\n// conn implements a connection which honors read and write timeouts.\ntype conn struct {\n\tc            net.Conn\n\tReadTimeout  time.Duration\n\tWriteTimeout time.Duration\n}\n\nfunc (c *conn) Read(b []byte) (int, error) {\n\tif c.ReadTimeout > 0 {\n\t\tc.c.SetReadDeadline(time.Now().Add(c.ReadTimeout))\n\t}\n\treturn c.c.Read(b)\n}\n\nfunc (c *conn) Write(b []byte) (int, error) {\n\tif c.WriteTimeout > 0 {\n\t\tc.c.SetWriteDeadline(time.Now().Add(c.WriteTimeout))\n\t}\n\treturn c.c.Write(b)\n}\n\nfunc (c *conn) Close() error {\n\treturn c.c.Close()\n}\n\nfunc (c *conn) LocalAddr() net.Addr {\n\treturn c.c.LocalAddr()\n}\n\nfunc (c *conn) RemoteAddr() net.Addr {\n\treturn c.c.RemoteAddr()\n}\n\nfunc (c *conn) SetDeadline(t time.Time) error {\n\treturn c.c.SetDeadline(t)\n}\n\nfunc (c *conn) SetReadDeadline(t time.Time) error {\n\treturn c.c.SetReadDeadline(t)\n}\n\nfunc (c *conn) SetWriteDeadline(t time.Time) error {\n\treturn c.c.SetWriteDeadline(t)\n}\n"
  },
  {
    "path": "proxy/tcp/sni_proxy.go",
    "content": "package tcp\n\nimport (\n\t\"bufio\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/route\"\n)\n\n// SNIProxy implements an SNI aware transparent TCP proxy which captures the\n// TLS client hello, extracts the host name and uses it for finding the\n// upstream server. Then it replays the ClientHello message and copies data\n// transparently allowing to route a TLS connection based on the SNI header\n// without decrypting it.\ntype SNIProxy struct {\n\n\t// Conn counts the number of connections.\n\tConn gkm.Counter\n\n\t// ConnFail counts the failed upstream connection attempts.\n\tConnFail gkm.Counter\n\n\t// Noroute counts the failed Lookup() calls.\n\tNoroute gkm.Counter\n\n\t// Lookup returns a target host for the given server name.\n\t// The proxy will panic if this value is nil.\n\tLookup func(host string) *route.Target\n\n\t// DialTimeout sets the timeout for establishing the outbound\n\t// connection.\n\tDialTimeout time.Duration\n}\n\nfunc (p *SNIProxy) ServeTCP(in net.Conn) error {\n\tdefer in.Close()\n\n\tif p.Conn != nil {\n\t\tp.Conn.Add(1)\n\t}\n\n\ttlsReader := bufio.NewReader(in)\n\ttlsHeaders, err := tlsReader.Peek(9)\n\tif err != nil {\n\t\tlog.Print(\"[DEBUG] tcp+sni: TLS handshake failed (failed to peek data)\")\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\n\tbufferSize, err := clientHelloBufferSize(tlsHeaders)\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] tcp+sni: TLS handshake failed (%s)\", err)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\n\tdata := make([]byte, bufferSize)\n\t_, err = io.ReadFull(tlsReader, data)\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] tcp+sni: TLS handshake failed (%s)\", err)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\n\t// readServerName wants only the handshake message so ignore the first\n\t// 5 bytes which is the TLS record header\n\thost, ok := readServerName(data[5:])\n\tif !ok {\n\t\tlog.Print(\"[DEBUG] tcp+sni: TLS handshake failed (unable to parse client hello)\")\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif host == \"\" {\n\t\tlog.Print(\"[DEBUG] tcp+sni: server_name missing\")\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn nil\n\t}\n\n\tt := p.Lookup(host)\n\tif t == nil {\n\t\tif p.Noroute != nil {\n\t\t\tp.Noroute.Add(1)\n\t\t}\n\t\treturn nil\n\t}\n\taddr := t.URL.Host\n\n\tif t.AccessDeniedTCP(in) {\n\t\treturn nil\n\t}\n\n\tout, err := net.DialTimeout(\"tcp\", addr, p.DialTimeout)\n\tif err != nil {\n\t\tlog.Print(\"[WARN] tcp+sni: cannot connect to upstream \", addr)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t// enable PROXY protocol support on outbound connection\n\tif t.ProxyProto {\n\t\terr := WriteProxyHeader(out, in)\n\t\tif err != nil {\n\t\t\tlog.Print(\"[WARN] tcp+sni: write proxy protocol header failed. \", err)\n\t\t\tif p.ConnFail != nil {\n\t\t\t\tp.ConnFail.Add(1)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// write the data already read from the connection\n\tn, err := out.Write(data)\n\tif err != nil {\n\t\tlog.Print(\"[WARN] tcp+sni: copy client hello failed. \", err)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\n\terrc := make(chan error, 2)\n\tcp := func(dst io.Writer, src io.Reader, c gkm.Counter) {\n\t\terrc <- copyBuffer(dst, src, c)\n\t}\n\n\t// we've received the ClientHello already\n\tif t.RxCounter != nil {\n\t\tt.RxCounter.Add(float64(n))\n\t}\n\n\tgo cp(in, out, t.RxCounter)\n\tgo cp(out, in, t.TxCounter)\n\terr = <-errc\n\tif err != nil && err != io.EOF {\n\t\tlog.Print(\"[WARN]: tcp+sni:  \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "proxy/tcp/tcp_dynamic_proxy.go",
    "content": "package tcp\n\nimport (\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/route\"\n)\n\n// Proxy implements a generic TCP proxying handler.\ntype DynamicProxy struct {\n\n\t// Conn counts the number of connections.\n\tConn gkm.Counter\n\n\t// ConnFail counts the failed upstream connection attempts.\n\tConnFail gkm.Counter\n\n\t// Noroute counts the failed Lookup() calls.\n\tNoroute gkm.Counter\n\n\t// Lookup returns a target host for the given request.\n\t// The proxy will panic if this value is nil.\n\tLookup func(host string) *route.Target\n\n\t// DialTimeout sets the timeout for establishing the outbound\n\t// connection.\n\tDialTimeout time.Duration\n}\n\nfunc (p *DynamicProxy) ServeTCP(in net.Conn) error {\n\tdefer in.Close()\n\n\tif p.Conn != nil {\n\t\tp.Conn.Add(1)\n\t}\n\n\ttarget := in.LocalAddr().String()\n\tt := p.Lookup(target)\n\tif t == nil {\n\t\t_, port, _ := net.SplitHostPort(target)\n\t\tt = p.Lookup(\":\" + port)\n\t}\n\tif t == nil {\n\t\tif p.Noroute != nil {\n\t\t\tp.Noroute.Add(1)\n\t\t}\n\t\treturn nil\n\t}\n\taddr := t.URL.Host\n\tlog.Printf(\"[DEBUG]  Connection: %s incoming %s to %s: \", in.RemoteAddr(), target, addr)\n\n\tif t.AccessDeniedTCP(in) {\n\t\treturn nil\n\t}\n\n\tout, err := net.DialTimeout(\"tcp\", addr, p.DialTimeout)\n\tif err != nil {\n\t\tlog.Print(\"[WARN] tcp: cannot connect to upstream \", addr)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\terrc := make(chan error, 2)\n\tcp := func(dst io.Writer, src io.Reader, c gkm.Counter) {\n\t\terrc <- copyBuffer(dst, src, c)\n\t}\n\n\tgo cp(in, out, t.RxCounter)\n\tgo cp(out, in, t.TxCounter)\n\terr = <-errc\n\tif err != nil && err != io.EOF {\n\t\tlog.Print(\"[WARN]: tcp:  \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "proxy/tcp/tcp_proxy.go",
    "content": "package tcp\n\nimport (\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/route\"\n)\n\n// Proxy implements a generic TCP proxying handler.\ntype Proxy struct {\n\n\t// Conn counts the number of connections.\n\tConn gkm.Counter\n\n\t// ConnFail counts the failed upstream connection attempts.\n\tConnFail gkm.Counter\n\n\t// Noroute counts the failed Lookup() calls.\n\tNoroute gkm.Counter\n\n\t// Lookup returns a target host for the given request.\n\t// The proxy will panic if this value is nil.\n\tLookup func(host string) *route.Target\n\n\t// DialTimeout sets the timeout for establishing the outbound\n\t// connection.\n\tDialTimeout time.Duration\n}\n\nfunc (p *Proxy) ServeTCP(in net.Conn) error {\n\tdefer in.Close()\n\n\tif p.Conn != nil {\n\t\tp.Conn.Add(1)\n\t}\n\n\t_, port, _ := net.SplitHostPort(in.LocalAddr().String())\n\tport = \":\" + port\n\tt := p.Lookup(port)\n\tif t == nil {\n\t\tif p.Noroute != nil {\n\t\t\tp.Noroute.Add(1)\n\t\t}\n\t\treturn nil\n\t}\n\taddr := t.URL.Host\n\n\tif t.AccessDeniedTCP(in) {\n\t\treturn nil\n\t}\n\n\tout, err := net.DialTimeout(\"tcp\", addr, p.DialTimeout)\n\tif err != nil {\n\t\tlog.Print(\"[WARN] tcp: cannot connect to upstream \", addr)\n\t\tif p.ConnFail != nil {\n\t\t\tp.ConnFail.Add(1)\n\t\t}\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t// enable PROXY protocol support on outbound connection\n\tif t.ProxyProto {\n\t\terr := WriteProxyHeader(out, in)\n\t\tif err != nil {\n\t\t\tlog.Print(\"[WARN] tcp: write proxy protocol header failed. \", err)\n\t\t\tif p.ConnFail != nil {\n\t\t\t\tp.ConnFail.Add(1)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\terrc := make(chan error, 2)\n\tcp := func(dst io.Writer, src io.Reader, c gkm.Counter) {\n\t\terrc <- copyBuffer(dst, src, c)\n\t}\n\n\tgo cp(in, out, t.RxCounter)\n\tgo cp(out, in, t.TxCounter)\n\terr = <-errc\n\tif err != nil && err != io.EOF {\n\t\tlog.Print(\"[WARN]: tcp:  \", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "proxy/tcp/tcptest/dialer.go",
    "content": "package tcptest\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"time\"\n)\n\ntype Dialer interface {\n\tDial(network, addr string) (net.Conn, error)\n}\n\nfunc NewRetryDialer() *RetryDialer {\n\treturn &RetryDialer{}\n}\n\n// RetryDialer retries the Dial function until it succeeds or\n// the timeout has been reached. The default timeout is one\n// second and the default sleep interval is 100ms.\ntype RetryDialer struct {\n\tDialer     net.Dialer\n\tTimeout    time.Duration\n\tSleep      time.Duration\n\tProxyProto bool\n}\n\nfunc (d *RetryDialer) Dial(network, addr string) (c net.Conn, err error) {\n\tdial := func() (net.Conn, error) {\n\t\tconn, err := d.Dialer.Dial(network, addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif d.ProxyProto {\n\t\t\tpxy := \"PROXY TCP4 1.2.3.4 5.6.7.8 12345 54321\\r\\n\"\n\t\t\tconn.Write([]byte(pxy))\n\t\t}\n\t\treturn conn, err\n\t}\n\treturn retry(dial, d.Timeout, d.Sleep)\n}\n\nfunc NewTLSRetryDialer(cfg *tls.Config) *TLSRetryDialer {\n\treturn &TLSRetryDialer{TLS: cfg}\n}\n\ntype TLSRetryDialer struct {\n\tTLS        *tls.Config\n\tDialer     net.Dialer\n\tTimeout    time.Duration\n\tSleep      time.Duration\n\tProxyProto bool\n}\n\nfunc (d *TLSRetryDialer) Dial(network, addr string) (c net.Conn, err error) {\n\tdial := func() (net.Conn, error) {\n\t\tconn, err := net.Dial(network, addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif d.ProxyProto {\n\t\t\tpxy := \"PROXY TCP4 1.2.3.4 5.6.7.8 12345 54321\\r\\n\"\n\t\t\tconn.Write([]byte(pxy))\n\t\t}\n\t\treturn tls.Client(conn, d.TLS), nil\n\t}\n\treturn retry(dial, d.Timeout, d.Sleep)\n}\n\ntype dialer func() (net.Conn, error)\n\nfunc retry(dial dialer, timeout, sleep time.Duration) (c net.Conn, err error) {\n\tif sleep == 0 {\n\t\tsleep = 100 * time.Millisecond\n\t}\n\tif timeout == 0 {\n\t\ttimeout = time.Second\n\t}\n\tdeadline := time.Now().Add(timeout)\n\n\tfor {\n\t\tc, err = dial()\n\t\tif err != nil && time.Now().Before(deadline) {\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "proxy/tcp/tcptest/server.go",
    "content": "package tcptest\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\tproxyproto \"github.com/armon/go-proxyproto\"\n\t\"github.com/fabiolb/fabio/proxy/internal\"\n\t\"github.com/fabiolb/fabio/proxy/tcp\"\n)\n\n// Server is a TCP test server that binds to a random port.\ntype Server struct {\n\tListener net.Listener\n\n\t// TLS is the optional TLS configuration, populated with a new config\n\t// after TLS is started. If set on an unstarted server before StartTLS\n\t// is called, existing fields are copied into the new config.\n\tTLS *tls.Config\n\n\t// Config may be changed after calling NewUnstartedServer and\n\t// before Start or StartTLS.\n\tConfig *tcp.Server\n\n\t// srv is the actual running server.\n\tsrv *tcp.Server\n\t// Addr is the address the server is listening on in the form ipaddr:port.\n\tAddr string\n}\n\nfunc (s *Server) Start() {\n\tif s.Addr != \"\" {\n\t\tpanic(\"Server already started\")\n\t}\n\n\ts.Addr = s.Listener.Addr().String()\n\ts.srv = new(tcp.Server)\n\ts.srv.Addr = s.Config.Addr\n\ts.srv.Handler = s.Config.Handler\n\ts.srv.ReadTimeout = s.Config.ReadTimeout\n\ts.srv.WriteTimeout = s.Config.WriteTimeout\n\tgo s.srv.Serve(s.Listener)\n}\n\nfunc (s *Server) StartTLS() {\n\tif s.Addr != \"\" {\n\t\tpanic(\"Server already started\")\n\t}\n\n\ts.Addr = s.Listener.Addr().String()\n\ts.srv = new(tcp.Server)\n\ts.srv.Addr = s.Config.Addr\n\ts.srv.Handler = s.Config.Handler\n\ts.srv.ReadTimeout = s.Config.ReadTimeout\n\ts.srv.WriteTimeout = s.Config.WriteTimeout\n\n\tcert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"tcptest: NewTLSServer: %v\", err))\n\t}\n\n\texistingConfig := s.TLS\n\tif existingConfig != nil {\n\t\ts.TLS = existingConfig.Clone()\n\t} else {\n\t\ts.TLS = new(tls.Config)\n\t}\n\tif len(s.TLS.Certificates) == 0 {\n\t\ts.TLS.Certificates = []tls.Certificate{cert}\n\t}\n\ts.Listener = tls.NewListener(s.Listener, s.TLS)\n\tgo s.srv.Serve(s.Listener)\n}\n\nfunc (s *Server) Close() error {\n\tif s.Addr == \"\" {\n\t\tpanic(\"Server not started\")\n\t}\n\treturn s.srv.Close()\n}\n\nfunc NewServer(h tcp.Handler) *Server {\n\tsrv := NewUnstartedServer(h)\n\tsrv.Start()\n\treturn srv\n}\n\nfunc NewTLSServer(h tcp.Handler) *Server {\n\tsrv := NewUnstartedServer(h)\n\tsrv.StartTLS()\n\treturn srv\n}\n\nfunc NewUnstartedServer(h tcp.Handler) *Server {\n\treturn &Server{\n\t\tListener: newLocalListener(),\n\t\tConfig:   &tcp.Server{Handler: h},\n\t}\n}\n\nfunc newLocalListener() net.Listener {\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tl, err = net.Listen(\"tcp6\", \"[::1]:0\")\n\t\tif err != nil {\n\t\t\tpanic(\"tcptest: Failed to listen on a port: \" + err.Error())\n\t\t}\n\t}\n\treturn l\n}\n\nfunc NewServerWithProxyProto(h tcp.Handler) *Server {\n\tsrv := NewUnstartedServerWithProxyProto(h)\n\tsrv.Start()\n\treturn srv\n}\n\nfunc NewTLSServerWithProxyProto(h tcp.Handler) *Server {\n\tsrv := NewUnstartedServerWithProxyProto(h)\n\tsrv.StartTLS()\n\treturn srv\n}\n\nfunc NewUnstartedServerWithProxyProto(h tcp.Handler) *Server {\n\treturn &Server{\n\t\tListener: &proxyproto.Listener{\n\t\t\tListener:           newLocalListener(),\n\t\t\tProxyHeaderTimeout: 100 * time.Millisecond,\n\t\t},\n\t\tConfig: &tcp.Server{Handler: h},\n\t}\n}\n"
  },
  {
    "path": "proxy/tcp/tls_clienthello.go",
    "content": "package tcp\n\nimport \"errors\"\n\n// Determines the required size of a buffer large enough to hold\n// a client hello message including the tls record header and the\n// handshake message header.\n// The function requires at least the first 9 bytes of the tls conversation\n// in \"data\".\n// An error is returned if the data does not follow the\n// specification (https://tools.ietf.org/html/rfc5246) or if the client hello\n// is fragmented over multiple records.\nfunc clientHelloBufferSize(data []byte) (int, error) {\n\t// TLS record header\n\t// -----------------\n\t// byte   0: rec type (should be 0x16 == Handshake)\n\t// byte 1-2: version (should be 0x3000 < v < 0x3003)\n\t// byte 3-4: rec len\n\tif len(data) < 9 {\n\t\treturn 0, errors.New(\"at least 9 bytes required to determine client hello length\")\n\t}\n\n\tif data[0] != 0x16 {\n\t\treturn 0, errors.New(\"not a TLS handshake\")\n\t}\n\n\trecordLength := int(data[3])<<8 | int(data[4])\n\tif recordLength <= 0 || recordLength > 16384 {\n\t\treturn 0, errors.New(\"invalid TLS record length\")\n\t}\n\n\t// Handshake record header\n\t// -----------------------\n\t// byte   5: hs msg type (should be 0x01 == client_hello)\n\t// byte 6-8: hs msg len\n\tif data[5] != 0x01 {\n\t\treturn 0, errors.New(\"not a client hello\")\n\t}\n\n\thandshakeLength := int(data[6])<<16 | int(data[7])<<8 | int(data[8])\n\tif handshakeLength <= 0 || handshakeLength > recordLength-4 {\n\t\treturn 0, errors.New(\"invalid client hello length (fragmentation not implemented)\")\n\t}\n\n\treturn handshakeLength + 9, nil //9 for the header bytes\n}\n\n// readServerName returns the server name from a TLS ClientHello message which\n// has the server_name extension (SNI). ok is set to true if the ClientHello\n// message was parsed successfully. If the server_name extension was not set\n// an empty string is returned as serverName.\n// clientHelloHandshakeMsg must contain the full client hello handshake\n// message including the 4 byte header.\n// See: https://www.ietf.org/rfc/rfc5246.txt\nfunc readServerName(clientHelloHandshakeMsg []byte) (serverName string, ok bool) {\n\tm := new(clientHelloMsg)\n\tif !m.unmarshal(clientHelloHandshakeMsg) {\n\t\t//println(\"client_hello unmarshal failed\")\n\t\treturn \"\", false\n\t}\n\n\treturn m.serverName, true\n}\n\n// The code below is a verbatim copy from go1.7/src/crypto/tls/handshake_messages.go\n// with some parts commented out. It does enough work to parse a TLS client hello\n// message and extract the server name extension since this is all we care about.\n//\n// Copyright (c) 2016 The Go Authors\n\n// TLS extension numbers\nconst (\n\textensionServerName uint16 = 0\n\t// extensionStatusRequest       uint16 = 5\n\t// extensionSupportedCurves     uint16 = 10\n\t// extensionSupportedPoints     uint16 = 11\n\t// extensionSignatureAlgorithms uint16 = 13\n\t// extensionALPN                uint16 = 16\n\t// extensionSCT                 uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6\n\t// extensionSessionTicket       uint16 = 35\n\t// extensionNextProtoNeg        uint16 = 13172 // not IANA assigned\n\t// extensionRenegotiationInfo   uint16 = 0xff01\n)\n\ntype clientHelloMsg struct {\n\tserverName         string\n\traw                []byte\n\trandom             []byte\n\tsessionId          []byte\n\tcompressionMethods []uint8\n\tsessionTicket      []uint8\n\talpnProtocols      []string\n\tvers               uint16\n\tnextProtoNeg       bool\n\tocspStapling       bool\n\tscts               bool\n\tticketSupported    bool\n}\n\nfunc (m *clientHelloMsg) unmarshal(data []byte) bool {\n\tif len(data) < 42 {\n\t\treturn false\n\t}\n\tm.raw = data\n\tm.vers = uint16(data[4])<<8 | uint16(data[5])\n\tm.random = data[6:38]\n\tsessionIdLen := int(data[38])\n\tif sessionIdLen > 32 || len(data) < 39+sessionIdLen {\n\t\treturn false\n\t}\n\tm.sessionId = data[39 : 39+sessionIdLen]\n\tdata = data[39+sessionIdLen:]\n\tif len(data) < 2 {\n\t\treturn false\n\t}\n\t// cipherSuiteLen is the number of bytes of cipher suite numbers. Since\n\t// they are uint16s, the number must be even.\n\tcipherSuiteLen := int(data[0])<<8 | int(data[1])\n\tif cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {\n\t\treturn false\n\t}\n\t// numCipherSuites := cipherSuiteLen / 2\n\t// m.cipherSuites = make([]uint16, numCipherSuites)\n\t// for i := 0; i < numCipherSuites; i++ {\n\t// \tm.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])\n\t// \tif m.cipherSuites[i] == scsvRenegotiation {\n\t// \t\tm.secureRenegotiationSupported = true\n\t// \t}\n\t// }\n\tdata = data[2+cipherSuiteLen:]\n\tif len(data) < 1 {\n\t\treturn false\n\t}\n\tcompressionMethodsLen := int(data[0])\n\tif len(data) < 1+compressionMethodsLen {\n\t\treturn false\n\t}\n\tm.compressionMethods = data[1 : 1+compressionMethodsLen]\n\n\tdata = data[1+compressionMethodsLen:]\n\n\tm.nextProtoNeg = false\n\tm.serverName = \"\"\n\tm.ocspStapling = false\n\tm.ticketSupported = false\n\tm.sessionTicket = nil\n\t// m.signatureAndHashes = nil\n\tm.alpnProtocols = nil\n\tm.scts = false\n\n\tif len(data) == 0 {\n\t\t// ClientHello is optionally followed by extension data\n\t\treturn true\n\t}\n\tif len(data) < 2 {\n\t\treturn false\n\t}\n\n\textensionsLength := int(data[0])<<8 | int(data[1])\n\tdata = data[2:]\n\tif extensionsLength != len(data) {\n\t\treturn false\n\t}\n\n\tfor len(data) != 0 {\n\t\tif len(data) < 4 {\n\t\t\treturn false\n\t\t}\n\t\textension := uint16(data[0])<<8 | uint16(data[1])\n\t\tlength := int(data[2])<<8 | int(data[3])\n\t\tdata = data[4:]\n\t\tif len(data) < length {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch extension {\n\t\tcase extensionServerName:\n\t\t\td := data[:length]\n\t\t\tif len(d) < 2 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tnamesLen := int(d[0])<<8 | int(d[1])\n\t\t\td = d[2:]\n\t\t\tif len(d) != namesLen {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tfor len(d) > 0 {\n\t\t\t\tif len(d) < 3 {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tnameType := d[0]\n\t\t\t\tnameLen := int(d[1])<<8 | int(d[2])\n\t\t\t\td = d[3:]\n\t\t\t\tif len(d) < nameLen {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif nameType == 0 {\n\t\t\t\t\tm.serverName = string(d[:nameLen])\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\td = d[nameLen:]\n\t\t\t}\n\t\t\t// case extensionNextProtoNeg:\n\t\t\t// \tif length > 0 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tm.nextProtoNeg = true\n\t\t\t// case extensionStatusRequest:\n\t\t\t// \tm.ocspStapling = length > 0 && data[0] == statusTypeOCSP\n\t\t\t// case extensionSupportedCurves:\n\t\t\t// \t// http://tools.ietf.org/html/rfc4492#section-5.5.1\n\t\t\t// \tif length < 2 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tl := int(data[0])<<8 | int(data[1])\n\t\t\t// \tif l%2 == 1 || length != l+2 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tnumCurves := l / 2\n\t\t\t// \tm.supportedCurves = make([]CurveID, numCurves)\n\t\t\t// \td := data[2:]\n\t\t\t// \tfor i := 0; i < numCurves; i++ {\n\t\t\t// \t\tm.supportedCurves[i] = CurveID(d[0])<<8 | CurveID(d[1])\n\t\t\t// \t\td = d[2:]\n\t\t\t// \t}\n\t\t\t// case extensionSupportedPoints:\n\t\t\t// \t// http://tools.ietf.org/html/rfc4492#section-5.5.2\n\t\t\t// \tif length < 1 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tl := int(data[0])\n\t\t\t// \tif length != l+1 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tm.supportedPoints = make([]uint8, l)\n\t\t\t// \tcopy(m.supportedPoints, data[1:])\n\t\t\t// case extensionSessionTicket:\n\t\t\t// \t// http://tools.ietf.org/html/rfc5077#section-3.2\n\t\t\t// \tm.ticketSupported = true\n\t\t\t// \tm.sessionTicket = data[:length]\n\t\t\t// case extensionSignatureAlgorithms:\n\t\t\t// \t// https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1\n\t\t\t// \tif length < 2 || length&1 != 0 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tl := int(data[0])<<8 | int(data[1])\n\t\t\t// \tif l != length-2 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tn := l / 2\n\t\t\t// \td := data[2:]\n\t\t\t// \tm.signatureAndHashes = make([]signatureAndHash, n)\n\t\t\t// \tfor i := range m.signatureAndHashes {\n\t\t\t// \t\tm.signatureAndHashes[i].hash = d[0]\n\t\t\t// \t\tm.signatureAndHashes[i].signature = d[1]\n\t\t\t// \t\td = d[2:]\n\t\t\t// \t}\n\t\t\t// case extensionRenegotiationInfo:\n\t\t\t// \tif length == 0 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \td := data[:length]\n\t\t\t// \tl := int(d[0])\n\t\t\t// \td = d[1:]\n\t\t\t// \tif l != len(d) {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\n\t\t\t// \tm.secureRenegotiation = d\n\t\t\t// \tm.secureRenegotiationSupported = true\n\t\t\t// case extensionALPN:\n\t\t\t// \tif length < 2 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \tl := int(data[0])<<8 | int(data[1])\n\t\t\t// \tif l != length-2 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t\t// \td := data[2:length]\n\t\t\t// \tfor len(d) != 0 {\n\t\t\t// \t\tstringLen := int(d[0])\n\t\t\t// \t\td = d[1:]\n\t\t\t// \t\tif stringLen == 0 || stringLen > len(d) {\n\t\t\t// \t\t\treturn false\n\t\t\t// \t\t}\n\t\t\t// \t\tm.alpnProtocols = append(m.alpnProtocols, string(d[:stringLen]))\n\t\t\t// \t\td = d[stringLen:]\n\t\t\t// \t}\n\t\t\t// case extensionSCT:\n\t\t\t// \tm.scts = true\n\t\t\t// \tif length != 0 {\n\t\t\t// \t\treturn false\n\t\t\t// \t}\n\t\t}\n\t\tdata = data[length:]\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "proxy/tcp/tls_clienthello_test.go",
    "content": "package tcp\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n)\n\nfunc TestClientHelloBufferSize(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\tsize int\n\t\tfail bool\n\t}{\n\t\t{\n\t\t\tname: \"valid data\",\n\t\t\t// Largest possible client hello message\n\t\t\t//                            |- 16384 -|      |----- 16380 ----|\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x3f, 0xfc},\n\t\t\tsize: 16384 + 5, // max record length + record header\n\t\t\tfail: false,\n\t\t},\n\t\t{\n\t\t\tname: \"not enough data\",\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x3f},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not a TLS record\",\n\t\t\tdata: []byte{0x15, 0x03, 0x01, 0x01, 0xF4, 0x01, 0x00, 0x01, 0xeb},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"TLS record too large\",\n\t\t\t//                             | max + 1 |\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x40, 0x01, 0x01, 0x00, 0x3f, 0xfc},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"TLS record length zero\",\n\t\t\t//                            |----------|\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x3f, 0xfc},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"Not a client hello\",\n\t\t\t//                                        |----|\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x02, 0x00, 0x3f, 0xfc},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"Invalid handshake message record length\",\n\t\t\t//                                              |----- 0 --------|\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x00, 0x00},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"Fragmentation (handshake message larger than record)\",\n\t\t\t//                            |-  500 ---|      |----- 497 ------|\n\t\t\tdata: []byte{0x16, 0x03, 0x01, 0x01, 0xF4, 0x01, 0x00, 0x01, 0xf1},\n\t\t\tsize: 0,\n\t\t\tfail: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := clientHelloBufferSize(tt.data)\n\n\t\t\tif tt.fail && err == nil {\n\t\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t\t} else if !tt.fail && err != nil {\n\t\t\t\tt.Fatalf(\"expected error to be nil, got %s\", err)\n\t\t\t}\n\n\t\t\tif want := tt.size; got != want {\n\t\t\t\tt.Fatalf(\"want size %d, got %d\", want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadServerName(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tservername string\n\t\tok         bool\n\t\tdata       string //Hex string, decoded by test\n\t}{\n\t\t{\n\t\t\t// Client hello from:\n\t\t\t// openssl s_client -connect google.com:443 -servername google.com\n\t\t\tname:       \"valid client hello with server name\",\n\t\t\tservername: \"google.com\",\n\t\t\tok:         true,\n\t\t\tdata: \"0100014803032657cacce41598fa82e5b75061050bc31c5affdba106b8e7431852\" +\n\t\t\t\t\"24af0fa1aa000098cc14cc13cc15c030c02cc028c024c014c00a00a3009f00\" +\n\t\t\t\t\"6b006a00390038ff8500c400c3008800870081c032c02ec02ac026c00fc005\" +\n\t\t\t\t\"009d003d003500c00084c02fc02bc027c023c013c00900a2009e0067004000\" +\n\t\t\t\t\"33003200be00bd00450044c031c02dc029c025c00ec004009c003c002f00ba\" +\n\t\t\t\t\"0041c011c007c00cc00200050004c012c00800160013c00dc003000a001500\" +\n\t\t\t\t\"12000900ff010000870000000f000d00000a676f6f676c652e636f6d000b00\" +\n\t\t\t\t\"0403000102000a003a0038000e000d0019001c000b000c001b00180009000a\" +\n\t\t\t\t\"001a0016001700080006000700140015000400050012001300010002000300\" +\n\t\t\t\t\"0f0010001100230000000d00260024060106020603efef0501050205030401\" +\n\t\t\t\t\"04020403eeeeeded030103020303020102020203\",\n\t\t},\n\t\t{\n\t\t\t// Client hello from:\n\t\t\t// openssl s_client -connect google.com:443\n\t\t\tname:       \"valid client hello but no server name extension\",\n\t\t\tservername: \"\",\n\t\t\tok:         true,\n\t\t\tdata: \"0100013503036dfb09de7b16503dd1bb304dcbe54079913b65abf53de997f73b26c99e\" +\n\t\t\t\t\"67ba28000098cc14cc13cc15c030c02cc028c024c014c00a00a3009f006b006a00\" +\n\t\t\t\t\"390038ff8500c400c3008800870081c032c02ec02ac026c00fc005009d003d0035\" +\n\t\t\t\t\"00c00084c02fc02bc027c023c013c00900a2009e006700400033003200be00bd00\" +\n\t\t\t\t\"450044c031c02dc029c025c00ec004009c003c002f00ba0041c011c007c00cc002\" +\n\t\t\t\t\"00050004c012c00800160013c00dc003000a00150012000900ff01000074000b00\" +\n\t\t\t\t\"0403000102000a003a0038000e000d0019001c000b000c001b00180009000a001a\" +\n\t\t\t\t\"00160017000800060007001400150004000500120013000100020003000f001000\" +\n\t\t\t\t\"1100230000000d00260024060106020603efef050105020503040104020403eeee\" +\n\t\t\t\t\"eded030103020303020102020203\",\n\t\t},\n\t\t{\n\t\t\tname:       \"invalid client hello\",\n\t\t\tservername: \"\",\n\t\t\tok:         false,\n\t\t\tdata: \"0100014c5768656e2070656f706c652073617920746f206d653a20776f756c6420796f\" +\n\t\t\t\t\"75207261746865722062652074686f75676874206f6620617320612066756e6e79\" +\n\t\t\t\t\"206d616e206f72206120677265617420626f73733f204d7920616e737765722773\" +\n\t\t\t\t\"20616c77617973207468652073616d652c20746f206d652c207468657927726520\" +\n\t\t\t\t\"6e6f74206d757475616c6c79206578636c75736976652e2d204461766964204272\" +\n\t\t\t\t\"656e74\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tclientHelloMsg, _ := hex.DecodeString(tt.data)\n\t\t\tservername, ok := readServerName(clientHelloMsg)\n\t\t\tif got, want := servername, tt.servername; got != want {\n\t\t\t\tt.Fatalf(\"%s: got servername \\\"%s\\\" want \\\"%s\\\"\", tt.name, got, want)\n\t\t\t}\n\n\t\t\tif got, want := ok, tt.ok; got != want {\n\t\t\t\tt.Fatalf(\"%s: got ok %t want %t\", tt.name, got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proxy/tcp_integration_test.go",
    "content": "package proxy\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/cert\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/proxy/internal\"\n\t\"github.com/fabiolb/fabio/proxy/tcp\"\n\t\"github.com/fabiolb/fabio/proxy/tcp/tcptest\"\n\t\"github.com/fabiolb/fabio/route\"\n)\n\nvar echoHandler tcp.HandlerFunc = func(c net.Conn) error {\n\tdefer c.Close()\n\tline, _, err := bufio.NewReader(c).ReadLine()\n\tif err != nil {\n\t\treturn err\n\t}\n\tline = append(line, []byte(\" echo\")...)\n\t_, err = c.Write(line)\n\treturn err\n}\n\n// TestTCPDynamicProxy tests proxying an unencrypted TCP connection\n// to a TCP upstream server.\nfunc TestTCPDyanmicProxy(t *testing.T) {\n\tsrv := tcptest.NewServer(echoHandler)\n\tdefer srv.Close()\n\n\t// start proxy\n\tproxyAddr := \"127.0.0.1:57778\"\n\tgo func() {\n\t\th := &tcp.DynamicProxy{\n\t\t\tLookup: func(h string) *route.Target {\n\t\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv 127.0.0.1:57778 tcp://\" + srv.Addr))\n\t\t\t\treturn tbl.LookupHost(h, route.Picker[\"rr\"])\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: proxyAddr}\n\t\tif err := ListenAndServeTCP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t}\n\t}()\n\tdefer Close()\n\n\t// connect to proxy\n\tout, err := tcptest.NewRetryDialer().Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"net.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestRoundtrip(t, out)\n}\n\n// TestTCPProxy tests proxying an unencrypted TCP connection\n// to a TCP upstream server.\nfunc TestTCPProxy(t *testing.T) {\n\tsrv := tcptest.NewServer(echoHandler)\n\tdefer srv.Close()\n\n\t// start proxy\n\tproxyAddr := \"127.0.0.1:57778\"\n\tgo func() {\n\t\th := &tcp.Proxy{\n\t\t\tLookup: func(h string) *route.Target {\n\t\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv :57778 tcp://\" + srv.Addr))\n\t\t\t\treturn tbl.LookupHost(h, route.Picker[\"rr\"])\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: proxyAddr}\n\t\tif err := ListenAndServeTCP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t}\n\t}()\n\tdefer Close()\n\n\t// connect to proxy\n\tout, err := tcptest.NewRetryDialer().Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"net.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestRoundtrip(t, out)\n}\n\n// TestTCPProxyWithTLS tests proxying an encrypted TCP connection\n// to an unencrypted upstream TCP server. The proxy terminates the\n// TLS connection.\nfunc TestTCPProxyWithTLS(t *testing.T) {\n\tsrv := tcptest.NewServer(echoHandler)\n\tdefer srv.Close()\n\n\t// setup cert source\n\tdir := t.TempDir()\n\n\tmustWrite := func(name string, data []byte) {\n\t\tpath := filepath.Join(dir, name)\n\t\tif err := os.WriteFile(path, data, 0644); err != nil {\n\t\t\tt.Fatalf(\"os.WriteFile: %s\", err)\n\t\t}\n\t}\n\tmustWrite(\"example.com-key.pem\", internal.LocalhostKey)\n\tmustWrite(\"example.com-cert.pem\", internal.LocalhostCert)\n\n\t// start tcp proxy\n\tproxyAddr := \"127.0.0.1:57779\"\n\tcs := config.CertSource{Name: \"cs\", Type: \"path\", CertPath: dir}\n\tsrc, err := cert.NewSource(cs)\n\tif err != nil {\n\t\tt.Fatal(\"cert.NewSource: \", err)\n\t}\n\ttlscfg, err := cert.TLSConfig(src, false, 0, 0, nil)\n\tif err != nil {\n\t\tt.Fatal(\"cert.TLSConfig: \", err)\n\t}\n\tgo func() {\n\n\t\th := &tcp.Proxy{\n\t\t\tLookup: func(string) *route.Target {\n\t\t\t\treturn &route.Target{URL: &url.URL{Host: srv.Addr}}\n\t\t\t},\n\t\t}\n\n\t\tl := config.Listen{Addr: proxyAddr}\n\t\tif err := ListenAndServeTCP(l, h, tlscfg); err != nil {\n\t\t\t// closing the listener returns this error from the accept loop\n\t\t\t// which we can ignore.\n\t\t\tif err.Error() != \"accept tcp 127.0.0.1:57779: use of closed network connection\" {\n\t\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t\t}\n\t\t}\n\t}()\n\tdefer Close()\n\n\t// give cert store some time to pick up certs\n\ttime.Sleep(250 * time.Millisecond)\n\n\trootCAs := x509.NewCertPool()\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {\n\t\tt.Fatal(\"could not parse cert\")\n\t}\n\tcfg := &tls.Config{\n\t\tRootCAs:    rootCAs,\n\t\tServerName: \"example.com\",\n\t}\n\n\t// connect to proxy\n\tout, err := tcptest.NewTLSRetryDialer(cfg).Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"tls.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestRoundtrip(t, out)\n}\n\n// TestTCPSNIProxy tests proxying an encrypted TCP connection\n// to an upstream TCP service without decrypting the traffic.\n// The upstream server terminates the TLS connection.\nfunc TestTCPSNIProxy(t *testing.T) {\n\tsrv := tcptest.NewTLSServer(echoHandler)\n\tdefer srv.Close()\n\n\t// start tcp proxy\n\tproxyAddr := \"127.0.0.1:57778\"\n\tgo func() {\n\t\th := &tcp.SNIProxy{\n\t\t\tLookup: func(string) *route.Target {\n\t\t\t\treturn &route.Target{URL: &url.URL{Host: srv.Addr}}\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: proxyAddr}\n\t\tif err := ListenAndServeTCP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t}\n\t}()\n\tdefer Close()\n\n\trootCAs := x509.NewCertPool()\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {\n\t\tt.Fatal(\"could not parse cert\")\n\t}\n\tcfg := &tls.Config{\n\t\tRootCAs:    rootCAs,\n\t\tServerName: \"example.com\",\n\t}\n\n\t// connect to proxy\n\tout, err := tcptest.NewTLSRetryDialer(cfg).Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"tls.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestRoundtrip(t, out)\n}\n\nfunc testRoundtrip(t *testing.T, c net.Conn) {\n\t// send data to server\n\t_, err := c.Write([]byte(\"foo\\n\"))\n\tif err != nil {\n\t\tt.Fatal(\"out.Write: \", err)\n\t}\n\n\t// read response which should be\n\t// src data + \" echo\"\n\tline, _, err := bufio.NewReader(c).ReadLine()\n\tif err != nil {\n\t\tt.Fatal(\"readLine: \", err)\n\t}\n\n\t// compare\n\tif got, want := line, []byte(\"foo echo\"); !bytes.Equal(got, want) {\n\t\tt.Fatalf(\"got %q want %q\", got, want)\n\t}\n}\n\nvar proxyHandler tcp.HandlerFunc = func(c net.Conn) error {\n\tdefer c.Close()\n\tline, _, err := bufio.NewReader(c).ReadLine()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstr := \" \" + c.RemoteAddr().String()\n\tline = append(line, []byte(str)...)\n\t_, err = c.Write(line)\n\treturn err\n}\n\n// TestTCPProxyWithProxyProtoEnables tests proxying an unencrypted TCP connection\n// to a TCP upstream server with proxy protocol enabed on upstream connection\nfunc TestTCPProxyWithProxyProto(t *testing.T) {\n\tsrv := tcptest.NewServerWithProxyProto(proxyHandler)\n\tdefer srv.Close()\n\n\t// start proxy\n\tproxyAddr := \"127.0.0.1:57778\"\n\tgo func() {\n\t\th := &tcp.Proxy{\n\t\t\tLookup: func(h string) *route.Target {\n\t\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(\"route add srv :57778 tcp://\" + srv.Addr + \" opts \\\"pxyproto=true\\\"\"))\n\t\t\t\ttgt := tbl.LookupHost(h, route.Picker[\"rr\"])\n\t\t\t\treturn tgt\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: proxyAddr, ProxyProto: true}\n\t\tif err := ListenAndServeTCP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t}\n\t}()\n\tdefer Close()\n\n\t// connect to proxy\n\tdialer := tcptest.NewRetryDialer()\n\tdialer.ProxyProto = true\n\tout, err := dialer.Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"net.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestProxyProto(t, out)\n}\n\n// TestTCPProxyWithTLSWithProxyProto tests proxying an encrypted TCP connection\n// to an unencrypted upstream TCP server with proxy protocol enabled.\n// The proxy extract the proxy protocol header and terminates the TLS connection.\nfunc TestTCPProxyWithTLSWithProxyProto(t *testing.T) {\n\tsrv := tcptest.NewServerWithProxyProto(proxyHandler)\n\tdefer srv.Close()\n\n\t// setup cert source\n\tdir := t.TempDir()\n\n\tmustWrite := func(name string, data []byte) {\n\t\tpath := filepath.Join(dir, name)\n\t\tif err := os.WriteFile(path, data, 0644); err != nil {\n\t\t\tt.Fatalf(\"os.WriteFile: %s\", err)\n\t\t}\n\t}\n\tmustWrite(\"example.com-key.pem\", internal.LocalhostKey)\n\tmustWrite(\"example.com-cert.pem\", internal.LocalhostCert)\n\n\t// start tcp proxy\n\tproxyAddr := \"127.0.0.1:57779\"\n\tcs := config.CertSource{Name: \"cs\", Type: \"path\", CertPath: dir}\n\tsrc, err := cert.NewSource(cs)\n\tif err != nil {\n\t\tt.Fatal(\"cert.NewSource: \", err)\n\t}\n\ttlscfg, err := cert.TLSConfig(src, false, 0, 0, nil)\n\tif err != nil {\n\t\tt.Fatal(\"cert.TLSConfig: \", err)\n\t}\n\tgo func() {\n\n\t\th := &tcp.Proxy{\n\t\t\tLookup: func(string) *route.Target {\n\t\t\t\treturn &route.Target{URL: &url.URL{Host: srv.Addr}, ProxyProto: true}\n\t\t\t},\n\t\t}\n\n\t\tl := config.Listen{Addr: proxyAddr, ProxyProto: true}\n\t\tif err := ListenAndServeTCP(l, h, tlscfg); err != nil {\n\t\t\t// closing the listener returns this error from the accept loop\n\t\t\t// which we can ignore.\n\t\t\tif err.Error() != \"accept tcp 127.0.0.1:57779: use of closed network connection\" {\n\t\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t\t}\n\t\t}\n\t}()\n\tdefer Close()\n\n\t// give cert store some time to pick up certs\n\ttime.Sleep(250 * time.Millisecond)\n\n\trootCAs := x509.NewCertPool()\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {\n\t\tt.Fatal(\"could not parse cert\")\n\t}\n\tcfg := &tls.Config{\n\t\tRootCAs:    rootCAs,\n\t\tServerName: \"example.com\",\n\t}\n\n\t// connect to proxy\n\tdialer := tcptest.NewTLSRetryDialer(cfg)\n\tdialer.ProxyProto = true\n\tout, err := dialer.Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"tls.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestProxyProto(t, out)\n}\n\n// TestTCPSNIProxyWithProxyProto tests proxying an encrypted TCP connection adding\n// proxy protocol header to an upstream TCP service without decrypting the traffic.\n// The upstream server extracts the proxy protocol and terminates the TLS connection.\nfunc TestTCPSNIProxyWithProxyProto(t *testing.T) {\n\tsrv := tcptest.NewTLSServerWithProxyProto(proxyHandler)\n\tdefer srv.Close()\n\n\t// start tcp proxy\n\tproxyAddr := \"127.0.0.1:57778\"\n\tgo func() {\n\t\th := &tcp.SNIProxy{\n\t\t\tLookup: func(string) *route.Target {\n\t\t\t\treturn &route.Target{URL: &url.URL{Host: srv.Addr}, ProxyProto: true}\n\t\t\t},\n\t\t}\n\t\tl := config.Listen{Addr: proxyAddr, ProxyProto: true}\n\t\tif err := ListenAndServeTCP(l, h, nil); err != nil {\n\t\t\tt.Log(\"ListenAndServeTCP: \", err)\n\t\t}\n\t}()\n\tdefer Close()\n\n\trootCAs := x509.NewCertPool()\n\tif ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {\n\t\tt.Fatal(\"could not parse cert\")\n\t}\n\tcfg := &tls.Config{\n\t\tRootCAs:    rootCAs,\n\t\tServerName: \"example.com\",\n\t}\n\n\t// connect to proxy\n\tdialer := tcptest.NewTLSRetryDialer(cfg)\n\tdialer.ProxyProto = true\n\tout, err := dialer.Dial(\"tcp\", proxyAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"tls.Dial: %#v\", err)\n\t}\n\tdefer out.Close()\n\n\ttestProxyProto(t, out)\n}\n\nfunc testProxyProto(t *testing.T, c net.Conn) {\n\t// send data to server\n\t_, err := c.Write([]byte(\"foo\\n\"))\n\tif err != nil {\n\t\tt.Fatal(\"out.Write: \", err)\n\t}\n\n\t// read response which should be\n\t// PROXY proto header\n\tline, _, err := bufio.NewReader(c).ReadLine()\n\tif err != nil {\n\t\tt.Fatal(\"readLine: \", err)\n\t}\n\n\t// remote := c.RemoteAddr().String()\n\t// local := c.LocalAddr().String()\n\n\t// compare\n\tif got, want := line, []byte(\"foo 1.2.3.4:12345\"); !bytes.Equal(got, want) {\n\t\tt.Fatalf(\"got %q want %q\", got, want)\n\t}\n}\n"
  },
  {
    "path": "proxy/ws_handler.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// conns keeps track of the number of open ws connections\nvar conns int64\n\ntype dialFunc func(network, address string) (net.Conn, error)\n\n// newWSHandler returns an HTTP handler which forwards data between\n// an incoming and outgoing websocket connection. It checks whether\n// the handshake was completed successfully before forwarding data\n// between the client and server.\nfunc newWSHandler(host string, dial dialFunc, conn gkm.Gauge) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif conn != nil {\n\t\t\tconn.Set(float64(atomic.AddInt64(&conns, 1)))\n\t\t\tdefer func() {\n\t\t\t\tconn.Set(float64(atomic.AddInt64(&conns, -1)))\n\t\t\t}()\n\t\t}\n\n\t\thj, ok := w.(http.Hijacker)\n\t\tif !ok {\n\t\t\thttp.Error(w, \"not a hijacker\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tin, _, err := hj.Hijack()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] Hijack error for %s. %s\", r.URL, err)\n\t\t\thttp.Error(w, \"hijack error\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tdefer in.Close()\n\n\t\tout, err := dial(\"tcp\", host)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] WS error for %s. %s\", r.URL, err)\n\t\t\thttp.Error(w, \"error contacting backend server\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tdefer out.Close()\n\n\t\terr = r.Write(out)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] Error copying request for %s. %s\", r.URL, err)\n\t\t\thttp.Error(w, \"error copying request\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// read the initial response to check whether we get an HTTP/1.1 101 ... response\n\t\t// to determine whether the handshake worked.\n\t\tb := make([]byte, 1024)\n\t\tif err := out.SetReadDeadline(time.Now().Add(time.Second)); err != nil {\n\t\t\tlog.Printf(\"[ERROR] Error setting read timeout for %s: %s\", r.URL, err)\n\t\t\thttp.Error(w, \"error setting read timeout\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tn, err := out.Read(b)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] Error reading handshake for %s: %s\", r.URL, err)\n\t\t\thttp.Error(w, \"error reading handshake\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tb = b[:n]\n\t\tif m, err := in.Write(b); err != nil || n != m {\n\t\t\tlog.Printf(\"[ERROR] Error sending handshake for %s: %s\", r.URL, err)\n\t\t\thttp.Error(w, \"error sending handshake\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// https://tools.ietf.org/html/rfc6455#section-1.3\n\t\t// The websocket server must respond with HTTP/1.1 101 on successful handshake\n\t\tif !bytes.HasPrefix(b, []byte(\"HTTP/1.1 101\")) {\n\t\t\tfirstLine := strings.SplitN(string(b), \"\\n\", 1)\n\t\t\tlog.Printf(\"[INFO] Websocket upgrade failed for %s: %s\", r.URL, firstLine)\n\t\t\thttp.Error(w, \"websocket upgrade failed\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tout.SetReadDeadline(time.Time{})\n\n\t\terrc := make(chan error, 2)\n\t\tcp := func(dst io.Writer, src io.Reader) {\n\t\t\t_, err := io.Copy(dst, src)\n\t\t\terrc <- err\n\t\t}\n\n\t\tgo cp(out, in)\n\t\tgo cp(in, out)\n\t\terr = <-errc\n\t\tif err != nil && err != io.EOF {\n\t\t\tlog.Printf(\"[INFO] WS error for %s. %s\", r.URL, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "proxy/ws_integration_test.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/route\"\n\n\t\"golang.org/x/net/websocket\"\n)\n\nfunc TestProxyWSUpstream(t *testing.T) {\n\twsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/ws\", \"/wss\", \"/insecure\", \"/strip\", \"/foo/bar/baz\", \"/new/world\":\n\t\t\twebsocket.Handler(wsEchoHandler).ServeHTTP(w, r)\n\t\tdefault:\n\t\t\tw.WriteHeader(404)\n\t\t}\n\t}))\n\tdefer wsServer.Close()\n\tt.Log(\"Started WS server: \", wsServer.URL)\n\n\twssServer := httptest.NewUnstartedServer(websocket.Handler(wsEchoHandler))\n\twssServer.TLS = tlsServerConfig()\n\twssServer.StartTLS()\n\tdefer wssServer.Close()\n\tt.Log(\"Started WSS server: \", wssServer.URL)\n\n\troutes := \"route add ws /ws  \" + wsServer.URL + \"\\n\"\n\troutes += \"route add ws /wss \" + wssServer.URL + ` opts \"proto=https\"` + \"\\n\"\n\troutes += \"route add ws /insecure \" + wssServer.URL + ` opts \"proto=https tlsskipverify=true\"` + \"\\n\"\n\troutes += \"route add ws /foo/strip  \" + wsServer.URL + ` opts \"strip=/foo\"` + \"\\n\"\n\troutes += \"route add ws /bar/baz  \" + wsServer.URL + ` opts \"prepend=/foo\"` + \"\\n\"\n\troutes += \"route add ws /old/world  \" + wsServer.URL + ` opts \"prepend=/new strip=/old\"` + \"\\n\"\n\n\thttpProxy := httptest.NewServer(&HTTPProxy{\n\t\tConfig:            config.Proxy{NoRouteStatus: 404, GZIPContentTypes: regexp.MustCompile(\".*\")},\n\t\tTransport:         &http.Transport{TLSClientConfig: tlsClientConfig()},\n\t\tInsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(routes))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\tdefer httpProxy.Close()\n\tt.Log(\"Started HTTP proxy: \", httpProxy.URL)\n\n\thttpsProxy := httptest.NewUnstartedServer(&HTTPProxy{\n\t\tConfig:            config.Proxy{NoRouteStatus: 404},\n\t\tTransport:         &http.Transport{TLSClientConfig: tlsClientConfig()},\n\t\tInsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()},\n\t\tLookup: func(r *http.Request) *route.Target {\n\t\t\ttbl, _ := route.NewTable(bytes.NewBufferString(routes))\n\t\t\treturn tbl.Lookup(r, route.Picker[\"rr\"], route.Matcher[\"prefix\"], globCache, globEnabled)\n\t\t},\n\t})\n\thttpsProxy.TLS = tlsServerConfig()\n\thttpsProxy.StartTLS()\n\tdefer httpsProxy.Close()\n\tt.Log(\"Started HTTPS proxy: \", httpsProxy.URL)\n\n\twsServerURL := wsServer.URL[len(\"http://\"):]\n\twssServerURL := wssServer.URL[len(\"https://\"):]\n\thttpProxyURL := httpProxy.URL[len(\"http://\"):]\n\thttpsProxyURL := httpsProxy.URL[len(\"https://\"):]\n\n\tt.Run(\"ws-ws direct\", func(t *testing.T) { testWSEcho(t, \"ws://\"+wsServerURL+\"/ws\", nil) })\n\tt.Run(\"wss-wss direct\", func(t *testing.T) { testWSEcho(t, \"wss://\"+wssServerURL+\"/wss\", nil) })\n\n\tt.Run(\"ws-ws via http proxy\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/ws\", nil) })\n\tt.Run(\"wss-ws via https proxy\", func(t *testing.T) { testWSEcho(t, \"wss://\"+httpsProxyURL+\"/ws\", nil) })\n\n\tt.Run(\"ws-wss via http proxy\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/wss\", nil) })\n\tt.Run(\"wss-wss via https proxy\", func(t *testing.T) { testWSEcho(t, \"wss://\"+httpsProxyURL+\"/wss\", nil) })\n\n\tt.Run(\"ws-wss tlsskipverify=true via http proxy\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/insecure\", nil) })\n\tt.Run(\"wss-wss tlsskipverify=true via https proxy\", func(t *testing.T) { testWSEcho(t, \"wss://\"+httpsProxyURL+\"/insecure\", nil) })\n\n\th := http.Header{\"Accept-Encoding\": []string{\"gzip\"}}\n\tt.Run(\"ws-ws via http proxy with gzip\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/ws\", h) })\n\n\tt.Run(\"ws-ws via http proxy with strip\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/foo/strip\", nil) })\n\n\tt.Run(\"ws-ws via http proxy with prepend\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/bar/baz\", nil) })\n\n\tt.Run(\"ws-ws via http proxy with strip & prepend\", func(t *testing.T) { testWSEcho(t, \"ws://\"+httpProxyURL+\"/old/world\", nil) })\n}\n\nfunc testWSEcho(t *testing.T, url string, hdr http.Header) {\n\tcfg, err := websocket.NewConfig(url, \"http://localhost/\")\n\tif err != nil {\n\t\tt.Fatal(\"NewConfig: \", err)\n\t}\n\tcfg.Header = hdr\n\tif strings.HasPrefix(url, \"wss://\") {\n\t\tcfg.TlsConfig = tlsClientConfig()\n\t}\n\tws, err := websocket.DialConfig(cfg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ws.Close()\n\n\tsend := []byte(\"foo\")\n\tif _, err := ws.Write([]byte(\"foo\")); err != nil {\n\t\tt.Logf(\"ws.Write failed: %s\", err)\n\t}\n\trecv := make([]byte, 100)\n\tn, err := ws.Read(recv)\n\tif err != nil {\n\t\tt.Logf(\"ws.Read failed: %s\", err)\n\t}\n\trecv = recv[:n]\n\tif got, want := recv, send; !bytes.Equal(got, want) {\n\t\tt.Fatalf(\"got %q want %q\", got, want)\n\t}\n}\n\nfunc wsEchoHandler(ws *websocket.Conn) {\n\tio.Copy(ws, ws)\n}\n"
  },
  {
    "path": "registry/backend.go",
    "content": "package registry\n\ntype Backend interface {\n\t// Register registers fabio as a service in the registry.\n\tRegister(services []string) error\n\n\t// Deregister removes all service registrations for fabio.\n\tDeregisterAll() error\n\n\t// Deregister removes the given service registration for fabio.\n\tDeregister(service string) error\n\n\t// ManualPaths returns the list of paths for which there\n\t// are overrides.\n\tManualPaths() ([]string, error)\n\n\t// ReadManual returns the current manual overrides and\n\t// their version as seen by the registry.\n\tReadManual(path string) (value string, version uint64, err error)\n\n\t// WriteManual writes the new value to the registry if the\n\t// version of the stored document still matchhes version.\n\tWriteManual(path string, value string, version uint64) (ok bool, err error)\n\n\t// WatchServices watches the registry for changes in service\n\t// registration and health and pushes them if there is a difference.\n\tWatchServices() chan string\n\n\t// WatchManual watches the registry for changes in the manual\n\t// overrides and pushes them if there is a difference.\n\tWatchManual() chan string\n\n\t// WatchNoRouteHTML watches the registry for changes in the html returned\n\t// when a requested route is not found\n\tWatchNoRouteHTML() chan string\n}\n\nvar Default Backend\n"
  },
  {
    "path": "registry/consul/backend.go",
    "content": "package consul\n\nimport (\n\t\"errors\"\n\t\"log\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/registry\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// be is an implementation of a registry backend for consul.\ntype be struct {\n\tc     *api.Client\n\tcfg   *config.Consul\n\tdereg map[string](chan bool)\n\tdc    string\n}\n\nfunc NewBackend(cfg *config.Consul) (registry.Backend, error) {\n\n\tconsulCfg := &api.Config{Address: cfg.Addr, Scheme: cfg.Scheme, Token: cfg.Token, Namespace: cfg.Namespace}\n\tif cfg.Scheme == \"https\" {\n\t\tconsulCfg.TLSConfig.KeyFile = cfg.TLS.KeyFile\n\t\tconsulCfg.TLSConfig.CertFile = cfg.TLS.CertFile\n\t\tconsulCfg.TLSConfig.CAFile = cfg.TLS.CAFile\n\t\tconsulCfg.TLSConfig.CAPath = cfg.TLS.CAPath\n\t\tconsulCfg.TLSConfig.InsecureSkipVerify = cfg.TLS.InsecureSkipVerify\n\t}\n\n\t// create a reusable client\n\tc, err := api.NewClient(consulCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// ping the agent\n\tdc, err := datacenter(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// we're good\n\tlog.Printf(\"[INFO] consul: Connecting to %q in datacenter %q\", cfg.Addr, dc)\n\tif cfg.Namespace != \"\" {\n\t\tlog.Printf(\"[INFO] consul: Connecting to namespace %q\", cfg.Namespace)\n\t} else {\n\t\tlog.Printf(\"[INFO] consul: Connecting to default namespace\")\n\t}\n\treturn &be{c: c, dc: dc, cfg: cfg}, nil\n}\n\nfunc (b *be) Register(services []string) error {\n\tif b.dereg == nil {\n\t\tb.dereg = make(map[string](chan bool))\n\t}\n\n\tif b.cfg.Register {\n\t\tservices = append(services, b.cfg.ServiceName)\n\t}\n\n\t// deregister unneeded services\n\tfor service := range b.dereg {\n\t\tif stringInSlice(service, services) {\n\t\t\tcontinue\n\t\t}\n\t\terr := b.Deregister(service)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// register new services\n\tfor _, service := range services {\n\t\tif b.dereg[service] != nil {\n\t\t\tlog.Printf(\"[DEBUG] %q already registered\", service)\n\t\t\tcontinue\n\t\t}\n\n\t\tserviceReg, err := serviceRegistration(b.cfg, service)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tb.dereg[service] = register(b.c, serviceReg)\n\t}\n\n\treturn nil\n}\n\nfunc (b *be) Deregister(service string) error {\n\tdereg := b.dereg[service]\n\tif dereg == nil {\n\t\tlog.Printf(\"[WARN]: Attempted to deregister unknown service %q\", service)\n\t\treturn nil\n\t}\n\tdereg <- true // trigger deregistration\n\t<-dereg       // wait for completion\n\tdelete(b.dereg, service)\n\n\treturn nil\n}\n\nfunc (b *be) DeregisterAll() error {\n\tlog.Printf(\"[DEBUG]: consul: Deregistering all registered aliases.\")\n\tfor _, dereg := range b.dereg {\n\t\tif dereg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tdereg <- true // trigger deregistration\n\t\t<-dereg       // wait for completion\n\t}\n\treturn nil\n}\n\nfunc (b *be) ManualPaths() ([]string, error) {\n\tkeys, _, err := listKeys(b.c, b.cfg.KVPath, 0, b.cfg.RequireConsistent, b.cfg.AllowStale)\n\treturn keys, err\n}\n\nfunc (b *be) ReadManual(path string) (value string, version uint64, err error) {\n\t// we cannot rely on the value provided by WatchManual() since\n\t// someone has to call that method first to kick off the go routine.\n\treturn getKV(b.c, b.cfg.KVPath+path, 0, b.cfg.RequireConsistent, b.cfg.AllowStale)\n}\n\nfunc (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) {\n\t// try to create the key first by using version 0\n\tif ok, err = putKV(b.c, b.cfg.KVPath+path, value, 0); ok {\n\t\treturn\n\t}\n\n\t// then try the CAS update\n\treturn putKV(b.c, b.cfg.KVPath+path, value, version)\n}\n\nfunc (b *be) WatchServices() chan string {\n\tlog.Printf(\"[INFO] consul: Using dynamic routes\")\n\tlog.Printf(\"[INFO] consul: Using tag prefix %q\", b.cfg.TagPrefix)\n\n\tm := NewServiceMonitor(b.c, b.cfg, b.dc)\n\tsvc := make(chan string)\n\tgo m.Watch(svc)\n\treturn svc\n}\n\nfunc (b *be) WatchManual() chan string {\n\tlog.Printf(\"[INFO] consul: Watching KV path %q\", b.cfg.KVPath)\n\n\tkv := make(chan string)\n\tgo watchKV(b.c, b.cfg.KVPath, kv, true, b.cfg.RequireConsistent, b.cfg.AllowStale)\n\treturn kv\n}\n\nfunc (b *be) WatchNoRouteHTML() chan string {\n\tlog.Printf(\"[INFO] consul: Watching KV path %q\", b.cfg.NoRouteHTMLPath)\n\n\thtml := make(chan string)\n\tgo watchKV(b.c, b.cfg.NoRouteHTMLPath, html, false, b.cfg.RequireConsistent, b.cfg.AllowStale)\n\treturn html\n}\n\n// datacenter returns the datacenter of the local agent\nfunc datacenter(c *api.Client) (string, error) {\n\tself, err := c.Agent().Self()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfg, ok := self[\"Config\"]\n\tif !ok {\n\t\treturn \"\", errors.New(\"consul: self.Config not found\")\n\t}\n\tdc, ok := cfg[\"Datacenter\"].(string)\n\tif !ok {\n\t\treturn \"\", errors.New(\"consul: self.Datacenter not found\")\n\t}\n\treturn dc, nil\n}\n\nfunc stringInSlice(str string, strSlice []string) bool {\n\tfor _, s := range strSlice {\n\t\tif s == str {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "registry/consul/kv.go",
    "content": "package consul\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// watchKV monitors a key in the KV store for changes.\n// The intended use case is to add additional route commands to the routing table.\nfunc watchKV(client *api.Client, path string, config chan string, separator bool, requireConsistent bool, allowStale bool) {\n\tvar lastIndex uint64\n\tvar lastValue string\n\n\tfor {\n\t\tvalue, index, err := listKV(client, path, lastIndex, separator, requireConsistent, allowStale)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] consul: Error fetching config from %s. %v\", path, err)\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\n\t\tif value != lastValue || index != lastIndex {\n\t\t\tlog.Printf(\"[DEBUG] consul: Manual config changed to #%d\", index)\n\t\t\tconfig <- value\n\t\t\tlastValue, lastIndex = value, index\n\t\t}\n\t}\n}\n\nfunc listKeys(client *api.Client, path string, waitIndex uint64, requireConsistent bool, allowStale bool) ([]string, uint64, error) {\n\tq := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex}\n\tkvpairs, meta, err := client.KV().List(path, q)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tif len(kvpairs) == 0 {\n\t\treturn nil, meta.LastIndex, nil\n\t}\n\tvar keys []string\n\tfor _, kvpair := range kvpairs {\n\t\tkeys = append(keys, kvpair.Key)\n\t}\n\treturn keys, meta.LastIndex, nil\n}\n\nfunc listKV(client *api.Client, path string, waitIndex uint64, separator bool, requireConsistent bool, allowStale bool) (string, uint64, error) {\n\tq := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex}\n\tkvpairs, meta, err := client.KV().List(path, q)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\tif len(kvpairs) == 0 {\n\t\treturn \"\", meta.LastIndex, nil\n\t}\n\tvar s []string\n\tfor _, kvpair := range kvpairs {\n\t\tval := strings.TrimSpace(string(kvpair.Value))\n\t\tif separator {\n\t\t\tval = \"# --- \" + kvpair.Key + \"\\n\" + val\n\t\t}\n\t\ts = append(s, val)\n\t}\n\treturn strings.Join(s, \"\\n\\n\"), meta.LastIndex, nil\n}\n\nfunc getKV(client *api.Client, key string, waitIndex uint64, requireConsistent bool, allowStale bool) (string, uint64, error) {\n\tq := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex}\n\tkvpair, meta, err := client.KV().Get(key, q)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\tif kvpair == nil {\n\t\treturn \"\", meta.LastIndex, nil\n\t}\n\treturn strings.TrimSpace(string(kvpair.Value)), meta.LastIndex, nil\n}\n\nfunc putKV(client *api.Client, key, value string, index uint64) (bool, error) {\n\tp := &api.KVPair{Key: key[1:], Value: []byte(value), ModifyIndex: index}\n\tok, _, err := client.KV().CAS(p, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn ok, nil\n}\n"
  },
  {
    "path": "registry/consul/passing.go",
    "content": "package consul\n\nimport (\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// passingServices takes a list of Consul Health Checks and only returns ones where the overall health of\n// the Service Instance is passing. This includes the health of the Node that the Services Instance runs on.\nfunc passingServices(checks []*api.HealthCheck, status []string, strict bool) []*api.HealthCheck {\n\tvar p []*api.HealthCheck\n\nCHECKS:\n\tfor _, svc := range checks {\n\t\tif !isServiceCheck(svc) {\n\t\t\tcontinue\n\t\t}\n\t\tvar total, passing int\n\n\t\tfor _, c := range checks {\n\t\t\tif svc.Node == c.Node {\n\t\t\t\tif svc.ServiceID == c.ServiceID {\n\t\t\t\t\ttotal++\n\t\t\t\t\tif hasStatus(c, status) {\n\t\t\t\t\t\tpassing++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif c.CheckID == \"serfHealth\" && c.Status == \"critical\" {\n\t\t\t\t\tlog.Printf(\"[DEBUG] consul: Skipping service %q since agent on node %q is down: %s\", c.ServiceID, c.Node, c.Output)\n\t\t\t\t\tcontinue CHECKS\n\t\t\t\t}\n\t\t\t\tif c.CheckID == \"_node_maintenance\" {\n\t\t\t\t\tlog.Printf(\"[DEBUG] consul: Skipping service %q since node %q is in maintenance mode: %s\", c.ServiceID, c.Node, c.Output)\n\t\t\t\t\tcontinue CHECKS\n\t\t\t\t}\n\t\t\t\tif c.CheckID == \"_service_maintenance:\"+svc.ServiceID && c.Status == \"critical\" {\n\t\t\t\t\tlog.Printf(\"[DEBUG] consul: Skipping service %q since it is in maintenance mode: %s\", svc.ServiceID, c.Output)\n\t\t\t\t\tcontinue CHECKS\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif passing == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif strict && total != passing {\n\t\t\tcontinue\n\t\t}\n\n\t\tp = append(p, svc)\n\t}\n\n\treturn p\n}\n\n// isServiceCheck returns true if the health check is a valid service check.\nfunc isServiceCheck(c *api.HealthCheck) bool {\n\treturn c.ServiceID != \"\" &&\n\t\tc.CheckID != \"serfHealth\" &&\n\t\tc.CheckID != \"_node_maintenance\" &&\n\t\t!strings.HasPrefix(c.CheckID, \"_service_maintenance:\")\n}\n\n// hasStatus returns true if the health check status is one of the given\n// values.\nfunc hasStatus(c *api.HealthCheck, status []string) bool {\n\tfor _, s := range status {\n\t\tif c.Status == s {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "registry/consul/passing_test.go",
    "content": "package consul\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\nfunc TestPassingServices(t *testing.T) {\n\tvar (\n\t\tserfPass      = &api.HealthCheck{Node: \"node\", CheckID: \"serfHealth\", Status: \"passing\"}\n\t\tserfFail      = &api.HealthCheck{Node: \"node\", CheckID: \"serfHealth\", Status: \"critical\"}\n\t\tsvc1Pass      = &api.HealthCheck{Node: \"node\", CheckID: \"service:abc\", Status: \"passing\", ServiceName: \"abc\", ServiceID: \"abc-1\"}\n\t\tsvc1Chk2Warn  = &api.HealthCheck{Node: \"node\", CheckID: \"service:abc\", Status: \"warning\", ServiceName: \"abc\", ServiceID: \"abc-1\"}\n\t\tsvc1Node2Pass = &api.HealthCheck{Node: \"node2\", CheckID: \"service:abc\", Status: \"passing\", ServiceName: \"abc\", ServiceID: \"abc-1\"}\n\t\tsvc1Warn      = &api.HealthCheck{Node: \"node\", CheckID: \"service:abc\", Status: \"warning\", ServiceName: \"abc\", ServiceID: \"abc-2\"}\n\t\tsvc1Crit      = &api.HealthCheck{Node: \"node\", CheckID: \"service:abc\", Status: \"critical\", ServiceName: \"abc\", ServiceID: \"abc-3\"}\n\t\tsvc2Pass      = &api.HealthCheck{Node: \"node\", CheckID: \"my-check-id\", Status: \"passing\", ServiceName: \"def\", ServiceID: \"def-1\"}\n\t\tsvc1Maint     = &api.HealthCheck{Node: \"node\", CheckID: \"_service_maintenance:abc-1\", Status: \"critical\", ServiceName: \"abc\", ServiceID: \"abc-1\"}\n\t\tsvc1ID2Maint  = &api.HealthCheck{Node: \"node\", CheckID: \"_service_maintenance:abc-2\", Status: \"critical\", ServiceName: \"abc\", ServiceID: \"abc-2\"}\n\t\tnodeMaint     = &api.HealthCheck{Node: \"node\", CheckID: \"_node_maintenance\", Status: \"critical\"}\n\t)\n\n\ttests := []struct {\n\t\tname    string\n\t\tstrict  bool\n\t\tstatus  []string\n\t\tin, out []*api.HealthCheck\n\t}{\n\t\t{\n\t\t\t\"expect no passing checks if checks array is nil\",\n\t\t\tfalse, []string{\"passing\"}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect no passing checks if checks array is empty\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect check to pass if it has a matching status\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Pass}, []*api.HealthCheck{svc1Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect all checks to pass if they have a matching status\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Pass, svc2Pass}, []*api.HealthCheck{svc1Pass, svc2Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect that internal consul checks are filtered out\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{serfPass, svc1Pass}, []*api.HealthCheck{svc1Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect no passing checks if consul agent is unhealthy\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{serfFail, svc1Pass}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect no passing checks if node is in maintenance mode\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{nodeMaint, svc1Pass}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect no passing check if corresponding service is in maintenance mode\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Maint, svc1Pass}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect no passing check if node and service are in maintenance mode\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{nodeMaint, svc1Maint, svc1Pass}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect no passing check if agent is unhealthy or node and service are in maintenance mode\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{serfFail, nodeMaint, svc1Maint, svc1Pass}, nil,\n\t\t},\n\t\t{\n\t\t\t\"expect check of service which is not in maintenance mode to pass if another instance of same service is in maintenance mode\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1ID2Maint, svc1Pass}, []*api.HealthCheck{svc1Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect that no checks of a service which is in maintenance mode are returned even if it has a passing check\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Maint, svc1Pass, svc2Pass}, []*api.HealthCheck{svc2Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect that a service's failing check does not affect a healthy instance of same service running on different node\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Crit, svc1Node2Pass}, []*api.HealthCheck{svc1Node2Pass},\n\t\t},\n\t\t{\n\t\t\t\"service in maintenance mode does not affect healthy service running on different node\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Maint, svc1Node2Pass}, []*api.HealthCheck{svc1Node2Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect that internal consul check and failing check are not returned\",\n\t\t\tfalse, []string{\"passing\", \"warning\"}, []*api.HealthCheck{serfPass, svc1Pass, svc1Crit}, []*api.HealthCheck{svc1Pass},\n\t\t},\n\t\t{\n\t\t\t\"expect that internal consul check is filtered out and check with warning is passing\",\n\t\t\tfalse, []string{\"passing\", \"warning\"}, []*api.HealthCheck{serfPass, svc1Warn, svc1Crit}, []*api.HealthCheck{svc1Warn},\n\t\t},\n\t\t{\n\t\t\t\"expect that warning and passing non-internal checks are returned\",\n\t\t\tfalse, []string{\"passing\", \"warning\"}, []*api.HealthCheck{serfPass, svc1Pass, svc1Warn}, []*api.HealthCheck{svc1Pass, svc1Warn},\n\t\t},\n\t\t{\n\t\t\t\"expect that warning und passing non-internal checks are returned\",\n\t\t\tfalse, []string{\"passing\", \"warning\"}, []*api.HealthCheck{serfPass, svc1Warn, svc1Crit, svc1Pass}, []*api.HealthCheck{svc1Warn, svc1Pass},\n\t\t},\n\t\t{\n\t\t\t\"in non-strict mode, expect that checks which belong to same service are passing, if at least one of them is passing\",\n\t\t\tfalse, []string{\"passing\"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn},\n\t\t},\n\t\t{\n\t\t\t\"in strict mode, expect that no checks which belong to same service are passing, if not all of them are passing\",\n\t\t\ttrue, []string{\"passing\"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, nil,\n\t\t},\n\t\t{\n\t\t\t\"in strict mode, expect that a failing check of one service does not affect a different service's passing check\",\n\t\t\ttrue, []string{\"passing\"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn, svc2Pass}, []*api.HealthCheck{svc2Pass},\n\t\t},\n\t\t{\n\t\t\t\"in strict mode, expect a check to pass if all of the other checks that belong to the same service are passing\",\n\t\t\ttrue, []string{\"passing\", \"warning\"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got, want := passingServices(tt.in, tt.status, tt.strict), tt.out; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "registry/consul/register.go",
    "content": "package consul\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/hashicorp/consul/api\"\n)\n\nconst (\n\tTTLInterval                       = time.Second * 15\n\tTTLRefreshInterval                = time.Second * 10\n\tTTLDeregisterCriticalServiceAfter = time.Minute\n)\n\n// register keeps a service registered in consul.\n//\n// When a value is sent in the dereg channel the service is deregistered from\n// consul. To wait for completion the caller should read the next value from\n// the dereg channel.\n//\n//\tdereg <- true // trigger deregistration\n//\t<-dereg       // wait for completion\nfunc register(c *api.Client, service *api.AgentServiceRegistration) chan bool {\n\tregistered := func(serviceID string) bool {\n\t\tif serviceID == \"\" {\n\t\t\treturn false\n\t\t}\n\t\tservices, err := c.Agent().Services()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[ERROR] consul: Cannot get service list. %s\", err)\n\t\t\treturn false\n\t\t}\n\t\treturn services[serviceID] != nil\n\t}\n\n\tregister := func() string {\n\t\tif err := c.Agent().ServiceRegister(service); err != nil {\n\t\t\tlog.Printf(\"[ERROR] consul: Cannot register fabio [name:%q] in Consul. %s\", service.Name, err)\n\t\t\treturn \"\"\n\t\t}\n\n\t\tlog.Printf(\"[INFO] consul: Registered fabio as %q\", service.Name)\n\t\tlog.Printf(\"[INFO] consul: Registered fabio with id %q\", service.ID)\n\t\tlog.Printf(\"[INFO] consul: Registered fabio with address %q\", service.Address)\n\t\tlog.Printf(\"[INFO] consul: Registered fabio with tags %q\", strings.Join(service.Tags, \",\"))\n\t\tfor _, check := range service.Checks {\n\t\t\tlog.Printf(\"[INFO] consul: Registered fabio with check %+v\", check)\n\t\t}\n\n\t\treturn service.ID\n\t}\n\n\tderegister := func(serviceID string) {\n\t\tlog.Printf(\"[INFO] consul: Deregistering %q\", service.Name)\n\t\tc.Agent().ServiceDeregister(serviceID)\n\t}\n\n\tpassTTL := func(serviceTTLID string) {\n\t\tc.Agent().UpdateTTL(serviceTTLID, \"\", api.HealthPassing)\n\t}\n\n\tdereg := make(chan bool)\n\tgo func() {\n\t\tvar serviceID string\n\t\tvar serviceTTLCheckId string\n\n\t\tfor {\n\t\t\tif !registered(serviceID) {\n\t\t\t\tserviceID = register()\n\t\t\t\tserviceTTLCheckId = computeServiceTTLCheckId(serviceID)\n\t\t\t\t// Pass the TTL check right now so traffic can be served immediately.\n\t\t\t\tpassTTL(serviceTTLCheckId)\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase <-dereg:\n\t\t\t\tderegister(serviceID)\n\t\t\t\tdereg <- true\n\t\t\t\treturn\n\t\t\tcase <-time.After(TTLRefreshInterval):\n\t\t\t\t// Reset the TTL check clock.\n\t\t\t\tpassTTL(serviceTTLCheckId)\n\t\t\t}\n\t\t}\n\t}()\n\treturn dereg\n}\n\nfunc serviceRegistration(cfg *config.Consul, serviceName string) (*api.AgentServiceRegistration, error) {\n\thostname, err := os.Hostname()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tipstr, portstr, err := net.SplitHostPort(cfg.ServiceAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tport, err := strconv.Atoi(portstr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tip := net.ParseIP(ipstr)\n\tif ip == nil {\n\t\tip, err = config.LocalIP()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif ip == nil {\n\t\t\treturn nil, errors.New(\"no local ip\")\n\t\t}\n\t}\n\n\tserviceID := fmt.Sprintf(\"%s-%s-%d\", serviceName, hostname, port)\n\n\tcheckURL := fmt.Sprintf(\"%s://%s:%d/health\", cfg.CheckScheme, ip, port)\n\tif ip.To4() == nil {\n\t\tcheckURL = fmt.Sprintf(\"%s://[%s]:%d/health\", cfg.CheckScheme, ip, port)\n\t}\n\n\tservice := &api.AgentServiceRegistration{\n\t\tID:        serviceID,\n\t\tName:      serviceName,\n\t\tAddress:   ip.String(),\n\t\tPort:      port,\n\t\tTags:      cfg.ServiceTags,\n\t\tNamespace: cfg.Namespace,\n\t\t// Set the checks for the service.\n\t\t//\n\t\t// Both checks must pass for Consul to consider the service healthy and therefore serve the fabio instance to clients.\n\t\tChecks: []*api.AgentServiceCheck{\n\t\t\t// If fabio doesn't exit cleanly, it doesn't auto-deregister itself\n\t\t\t// from Consul. In order to address this, we introduce a TTL check\n\t\t\t// to confirm that the fabio instance is alive and able to route\n\t\t\t// this service.\n\t\t\t//\n\t\t\t// The TTL check must be refreshed before its timeout is crossed,\n\t\t\t// otherwise the check fails. If the check fails, Consul considers\n\t\t\t// this service to have become unhealthy. If the check is failing\n\t\t\t// (critical) for the DeregisterCriticalServiceAfter duration, the\n\t\t\t// Consul reaper will remove it from Consul.\n\t\t\t//\n\t\t\t// For more info, read https://www.consul.io/api/agent/check.html#deregistercriticalserviceafter.\n\t\t\t{\n\t\t\t\tCheckID:                        computeServiceTTLCheckId(serviceID),\n\t\t\t\tTTL:                            TTLInterval.String(),\n\t\t\t\tDeregisterCriticalServiceAfter: TTLDeregisterCriticalServiceAfter.String(),\n\t\t\t},\n\t\t\t// HTTP check is meant to confirm fabio health endpoint is\n\t\t\t// reachable from the Consul agent. If the check fails, Consul\n\t\t\t// considers this service to have become unhealthy.\n\t\t\t{\n\t\t\t\tHTTP:          checkURL,\n\t\t\t\tInterval:      cfg.CheckInterval.String(),\n\t\t\t\tTimeout:       cfg.CheckTimeout.String(),\n\t\t\t\tTLSSkipVerify: cfg.CheckTLSSkipVerify,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn service, nil\n}\n\nfunc computeServiceTTLCheckId(serviceID string) string {\n\treturn serviceID + \"-ttl\"\n}\n"
  },
  {
    "path": "registry/consul/routecmd.go",
    "content": "package consul\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// routecmd builds a route command.\ntype routecmd struct {\n\t// svc is the consul service instance.\n\tsvc *api.CatalogService\n\n\tenv map[string]string\n\n\t// prefix is the prefix of urlprefix tags. e.g. 'urlprefix-'.\n\tprefix string\n}\n\nfunc (r routecmd) build() []string {\n\tvar svctags, routetags []string\n\tfor _, t := range r.svc.ServiceTags {\n\n\t\tt = strings.TrimSpace(t)\n\n\t\tif strings.HasPrefix(t, r.prefix) {\n\t\t\troutetags = append(routetags, t)\n\t\t} else {\n\t\t\tsvctags = append(svctags, t)\n\t\t}\n\t}\n\n\t// generate route commands\n\tvar config []string\n\tfor _, tag := range routetags {\n\t\tif route, opts, ok := parseURLPrefixTag(tag, r.prefix, r.env); ok {\n\t\t\tname, addr, port := r.svc.ServiceName, r.svc.ServiceAddress, r.svc.ServicePort\n\n\t\t\t// use consul node address if service address is not set\n\t\t\tif addr == \"\" {\n\t\t\t\taddr = r.svc.Address\n\t\t\t}\n\n\t\t\t// add .local suffix on OSX for simple host names w/o domain\n\t\t\tif runtime.GOOS == \"darwin\" && !strings.Contains(addr, \".\") && !strings.HasSuffix(addr, \".local\") {\n\t\t\t\taddr += \".local\"\n\t\t\t}\n\n\t\t\taddr = net.JoinHostPort(addr, strconv.Itoa(port))\n\t\t\t//tags := strings.Join(r.tags, \",\")\n\t\t\tdst := \"http://\" + addr + \"/\"\n\n\t\t\tvar weight string\n\t\t\tvar ropts []string\n\t\t\tfor _, o := range strings.Fields(opts) {\n\t\t\t\tswitch {\n\t\t\t\tcase o == \"proto=tcp\":\n\t\t\t\t\tdst = \"tcp://\" + addr\n\n\t\t\t\tcase o == \"proto=https\":\n\t\t\t\t\tdst = \"https://\" + addr\n\n\t\t\t\tcase o == \"proto=grpcs\":\n\t\t\t\t\tdst = \"grpcs://\" + addr\n\n\t\t\t\tcase o == \"proto=grpc\":\n\t\t\t\t\tdst = \"grpc://\" + addr\n\n\t\t\t\tcase strings.HasPrefix(o, \"weight=\"):\n\t\t\t\t\tweight = o[len(\"weight=\"):]\n\n\t\t\t\tcase strings.HasPrefix(o, \"redirect=\"):\n\t\t\t\t\tredir := strings.Split(o[len(\"redirect=\"):], \",\")\n\t\t\t\t\tif len(redir) == 2 {\n\t\t\t\t\t\tdst = redir[1]\n\t\t\t\t\t\tropts = append(ropts, fmt.Sprintf(\"redirect=%s\", redir[0]))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Printf(\"[ERROR] Invalid syntax for redirect: %s. should be redirect=<code>,<url>\", o)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tropts = append(ropts, o)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcfg := \"route add \" + name + \" \" + route + \" \" + dst\n\t\t\tif weight != \"\" {\n\t\t\t\tcfg += \" weight \" + weight\n\t\t\t}\n\t\t\tif len(svctags) > 0 {\n\t\t\t\tcfg += \" tags \" + strconv.Quote(strings.Join(svctags, \",\"))\n\t\t\t}\n\t\t\tif len(ropts) > 0 {\n\t\t\t\tcfg += \" opts \" + strconv.Quote(strings.Join(ropts, \" \"))\n\t\t\t}\n\n\t\t\tconfig = append(config, cfg)\n\t\t}\n\t}\n\treturn config\n}\n\n// parseURLPrefixTag expects an input in the form of 'tag-host/path[ opts]'\n// and returns the lower cased host and the unaltered path if the\n// prefix matches the tag.\nfunc parseURLPrefixTag(s, prefix string, env map[string]string) (route, opts string, ok bool) {\n\t// expand $x or ${x} to env[x] or \"\"\n\texpand := func(s string) string {\n\t\treturn os.Expand(s, func(x string) string {\n\t\t\tif env == nil {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn env[x]\n\t\t})\n\t}\n\n\ts = strings.TrimSpace(s)\n\tif !strings.HasPrefix(s, prefix) {\n\t\treturn \"\", \"\", false\n\t}\n\ts = strings.TrimSpace(s[len(prefix):])\n\n\tp := strings.SplitN(s, \" \", 2)\n\tif len(p) == 2 {\n\t\topts = p[1]\n\t}\n\ts = p[0]\n\n\t// prefix is \":port\"\n\tif strings.HasPrefix(s, \":\") {\n\t\treturn s, opts, true\n\t}\n\n\tif !strings.Contains(s, \"/\") {\n\t\treturn s, opts, true\n\t}\n\t// prefix is \"host/path\"\n\tp = strings.SplitN(s, \"/\", 2)\n\tif len(p) == 1 {\n\t\tlog.Printf(\"[WARN] consul: Invalid %s tag %q - You need to have a trailing slash!\", prefix, s)\n\t\treturn \"\", \"\", false\n\t}\n\thost, path := p[0], p[1]\n\n\treturn strings.ToLower(expand(host)) + \"/\" + expand(path), opts, true\n}\n"
  },
  {
    "path": "registry/consul/routecmd_test.go",
    "content": "package consul\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/consul/api\"\n)\n\nfunc TestRouteCmd(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tr    routecmd\n\t\tcfg  []string\n\t}{\n\t\t{\n\t\t\tname: \"http\",\n\t\t\tr: routecmd{\n\t\t\t\tprefix: \"p-\",\n\t\t\t\tsvc: &api.CatalogService{\n\t\t\t\t\tServiceName:    \"svc-1\",\n\t\t\t\t\tServiceAddress: \"1.1.1.1\",\n\t\t\t\t\tServicePort:    2222,\n\t\t\t\t\tServiceTags:    []string{`p-foo/bar`},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcfg: []string{\n\t\t\t\t`route add svc-1 foo/bar http://1.1.1.1:2222/`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http single tag with a space\",\n\t\t\tr: routecmd{\n\t\t\t\tprefix: \"p-\",\n\t\t\t\tsvc: &api.CatalogService{\n\t\t\t\t\tServiceName:    \"svc-1\",\n\t\t\t\t\tServiceAddress: \"1.1.1.1\",\n\t\t\t\t\tServicePort:    2222,\n\t\t\t\t\tServiceTags:    []string{` p-foo/bar`},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcfg: []string{\n\t\t\t\t`route add svc-1 foo/bar http://1.1.1.1:2222/`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http multiple tags\",\n\t\t\tr: routecmd{\n\t\t\t\tprefix: \"p-\",\n\t\t\t\tsvc: &api.CatalogService{\n\t\t\t\t\tServiceName:    \"svc-1\",\n\t\t\t\t\tServiceAddress: \"1.1.1.1\",\n\t\t\t\t\tServicePort:    2222,\n\t\t\t\t\tServiceTags:    []string{`p-foo/bar`, `p-test/foo`},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcfg: []string{\n\t\t\t\t`route add svc-1 foo/bar http://1.1.1.1:2222/`,\n\t\t\t\t`route add svc-1 test/foo http://1.1.1.1:2222/`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http multiple routes with space prefix route\",\n\t\t\tr: routecmd{\n\t\t\t\tprefix: \"p-\",\n\t\t\t\tsvc: &api.CatalogService{\n\t\t\t\t\tServiceName:    \"svc-1\",\n\t\t\t\t\tServiceAddress: \"1.1.1.1\",\n\t\t\t\t\tServicePort:    2222,\n\t\t\t\t\tServiceTags:    []string{`p-foo/bar`, `  p-test/foo`},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcfg: []string{\n\t\t\t\t`route add svc-1 foo/bar http://1.1.1.1:2222/`,\n\t\t\t\t`route add svc-1 test/foo http://1.1.1.1:2222/`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tcp\",\n\t\t\tr: routecmd{\n\t\t\t\tprefix: \"p-\",\n\t\t\t\tsvc: &api.CatalogService{\n\t\t\t\t\tServiceName:    \"svc-1\",\n\t\t\t\t\tServiceAddress: \"1.1.1.1\",\n\t\t\t\t\tServicePort:    2222,\n\t\t\t\t\tServiceTags:    []string{`p-:1234 proto=tcp`},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcfg: []string{\n\t\t\t\t`route add svc-1 :1234 tcp://1.1.1.1:2222`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tif got, want := c.r.build(), c.cfg; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Fatalf(\"\\ngot  %#v\\nwant %#v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseTag(t *testing.T) {\n\tprefix := \"p-\"\n\ttests := []struct {\n\t\ttag   string\n\t\tenv   map[string]string\n\t\troute string\n\t\topts  string\n\t\tok    bool\n\t}{\n\t\t{tag: \"p\", route: \"\", ok: false},\n\t\t{tag: \"p-\", route: \"\", ok: true},\n\t\t{tag: \"p- \", route: \"\", ok: true},\n\t\t{tag: \"p-/\", route: \"/\", ok: true},\n\t\t{tag: \" p-/\", route: \"/\", ok: true},\n\t\t{tag: \"p-/ \", route: \"/\", ok: true},\n\t\t{tag: \"p- / \", route: \"/\", ok: true},\n\t\t{tag: \"p-/foo\", route: \"/foo\", ok: true},\n\t\t{tag: \"p- /foo\", route: \"/foo\", ok: true},\n\t\t{tag: \"p-1.1.1.1:999\", route: \"1.1.1.1:999\", ok: true},\n\t\t{tag: \"p-bar/foo\", route: \"bar/foo\", ok: true},\n\t\t{tag: \"p-bar/foo/foo\", route: \"bar/foo/foo\", ok: true},\n\t\t{tag: \"p-www.bar.com/foo/foo\", route: \"www.bar.com/foo/foo\", ok: true},\n\t\t{tag: \"p-WWW.BAR.COM/foo/foo\", route: \"www.bar.com/foo/foo\", ok: true},\n\t\t{tag: \"p-bar/foo a b c\", route: \"bar/foo\", opts: \"a b c\", ok: true},\n\t\t{\n\t\t\ttag:   \"p-$x/$y\",\n\t\t\troute: \"/\",\n\t\t\tok:    true,\n\t\t},\n\t\t{\n\t\t\ttag:   \"p-${x}/${y}\",\n\t\t\troute: \"/\",\n\t\t\tok:    true,\n\t\t},\n\t\t{\n\t\t\ttag:   \"p-$x/$Y\",\n\t\t\tenv:   map[string]string{\"x\": \"Xx\", \"Y\": \"Yy\"},\n\t\t\troute: \"xx/Yy\",\n\t\t\tok:    true,\n\t\t},\n\t\t{\n\t\t\ttag:   \"p-${x}/${Y}\",\n\t\t\tenv:   map[string]string{\"x\": \"Xx\", \"Y\": \"Yy\"},\n\t\t\troute: \"xx/Yy\",\n\t\t\tok:    true,\n\t\t},\n\t\t{\n\t\t\ttag:   \"p-www.bar.com:80/foo redirect=302,https://www.bar.com\",\n\t\t\troute: \"www.bar.com:80/foo\",\n\t\t\topts:  \"redirect=302,https://www.bar.com\",\n\t\t\tok:    true,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\turi, opts, ok := parseURLPrefixTag(tt.tag, prefix, tt.env)\n\t\tif got, want := ok, tt.ok; got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif got, want := uri, tt.route; got != want {\n\t\t\tt.Errorf(\"%d: got uri %q want %q\", i, got, want)\n\t\t}\n\t\tif got, want := opts, tt.opts; got != want {\n\t\t\tt.Errorf(\"%d: got opts %q want %q\", i, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "registry/consul/service.go",
    "content": "package consul\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/hashicorp/consul/api\"\n)\n\n// ServiceMonitor generates fabio configurations from consul state.\ntype ServiceMonitor struct {\n\tclient *api.Client\n\tconfig *config.Consul\n\tdc     string\n\tstrict bool\n}\n\nfunc NewServiceMonitor(client *api.Client, config *config.Consul, dc string) *ServiceMonitor {\n\treturn &ServiceMonitor{\n\t\tclient: client,\n\t\tconfig: config,\n\t\tdc:     dc,\n\t\tstrict: config.ChecksRequired == \"all\",\n\t}\n}\n\n// Watch monitors the consul health checks and sends a new\n// configuration to the updates channel on every change.\nfunc (w *ServiceMonitor) Watch(updates chan string) {\n\tvar lastIndex uint64\n\tvar q *api.QueryOptions\n\tfor {\n\t\tif w.config.PollInterval != 0 {\n\t\t\tq = &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale}\n\t\t\ttime.Sleep(w.config.PollInterval)\n\t\t} else {\n\t\t\tq = &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale, WaitIndex: lastIndex}\n\t\t}\n\t\tchecks, meta, err := w.client.Health().State(\"any\", q)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[WARN] consul: Error fetching health state. %v\", err)\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Printf(\"[DEBUG] consul: Health changed to #%d\", meta.LastIndex)\n\n\t\tprefixedChecks := checksWithTagPrefix(w.config.TagPrefix, checks)\n\t\tlog.Printf(\"[DEBUG] consul: only %d of %d checks have the configured tag prefix\", len(prefixedChecks), len(checks))\n\n\t\t// determine which services have passing health checks\n\t\tpassing := passingServices(prefixedChecks, w.config.ServiceStatus, w.strict)\n\n\t\t// build the config for the passing services\n\t\tupdates <- w.makeConfig(passing)\n\n\t\t// remember the last state and wait for the next change\n\t\tlastIndex = meta.LastIndex\n\t}\n}\n\n// makeConfig determines which service instances have passing health checks\n// and then finds the ones which have tags with the right prefix to build the config from.\nfunc (w *ServiceMonitor) makeConfig(checks []*api.HealthCheck) string {\n\t// map service name to list of service passing for which the health check is ok\n\tm := map[string]map[string]bool{}\n\tfor _, check := range checks {\n\t\t// Make the node part of the id, because according to the Consul docs\n\t\t// the ServiceID is unique per agent but not cluster wide\n\t\t// https://www.consul.io/api/agent/service.html#id\n\t\tname, id := check.ServiceName, fmt.Sprintf(\"%s.%s\", check.Node, check.ServiceID)\n\n\t\tif _, ok := m[name]; !ok {\n\t\t\tm[name] = map[string]bool{}\n\t\t}\n\t\tm[name][id] = true\n\t}\n\n\tn := w.config.ServiceMonitors\n\tif n <= 0 {\n\t\tn = 1\n\t}\n\n\tsem := make(chan int, n)\n\tcfgs := make(chan []string, len(m))\n\tfor name, passing := range m {\n\t\tname, passing := name, passing\n\t\tgo func() {\n\t\t\tsem <- 1\n\t\t\tcfgs <- w.serviceConfig(name, passing)\n\t\t\t<-sem\n\t\t}()\n\t}\n\n\tvar config []string\n\tfor range m {\n\t\tcfg := <-cfgs\n\t\tconfig = append(config, cfg...)\n\t}\n\n\t// sort config in reverse order to sort most specific config to the top\n\tsort.Sort(sort.Reverse(sort.StringSlice(config)))\n\n\treturn strings.Join(config, \"\\n\")\n}\n\n// serviceConfig constructs the config for all good instances of a single service.\nfunc (w *ServiceMonitor) serviceConfig(name string, passing map[string]bool) (config []string) {\n\tif name == \"\" || len(passing) == 0 {\n\t\treturn nil\n\t}\n\n\tq := &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale}\n\tsvcs, _, err := w.client.Catalog().Service(name, \"\", q)\n\tif err != nil {\n\t\tlog.Printf(\"[WARN] consul: Error getting catalog service %s. %v\", name, err)\n\t\treturn nil\n\t}\n\n\tenv := map[string]string{\n\t\t\"DC\": w.dc,\n\t}\n\n\tfor _, svc := range svcs {\n\t\t// check if this instance passed the health check\n\t\tif _, ok := passing[svc.Node+\".\"+svc.ServiceID]; !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tr := routecmd{\n\t\t\tsvc:    svc,\n\t\t\tenv:    env,\n\t\t\tprefix: w.config.TagPrefix,\n\t\t}\n\t\tcmds := r.build()\n\n\t\tconfig = append(config, cmds...)\n\t}\n\treturn config\n}\n\n// checksWithTagPrefix filters a list of Consul Health Checks to only the Checks with a Tag that begins with the prefix\nfunc checksWithTagPrefix(prefix string, checks api.HealthChecks) api.HealthChecks {\n\tchecksWithPrefix := make(api.HealthChecks, 0, len(checks))\n\tfor _, c := range checks {\n\t\tif c.CheckID == \"serfHealth\" || c.CheckID == \"_node_maintenance\" || strings.HasPrefix(c.CheckID, \"_service_maintenance\") {\n\t\t\tchecksWithPrefix = append(checksWithPrefix, c)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, t := range c.ServiceTags {\n\t\t\tif strings.HasPrefix(t, prefix) {\n\t\t\t\tchecksWithPrefix = append(checksWithPrefix, c)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn checksWithPrefix\n}\n"
  },
  {
    "path": "registry/custom/backend.go",
    "content": "package custom\n\nimport (\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/registry\"\n\t\"log\"\n)\n\ntype be struct {\n\tcfg *config.Custom\n}\n\nfunc NewBackend(cfg *config.Custom) (registry.Backend, error) {\n\treturn &be{cfg}, nil\n}\n\nfunc (b *be) Register(services []string) error {\n\treturn nil\n}\n\nfunc (b *be) Deregister(serviceName string) error {\n\treturn nil\n}\n\nfunc (b *be) DeregisterAll() error {\n\treturn nil\n}\n\nfunc (b *be) ManualPaths() ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (b *be) ReadManual(string) (value string, version uint64, err error) {\n\treturn \"\", 0, nil\n}\n\nfunc (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) {\n\treturn false, nil\n}\n\nfunc (b *be) WatchServices() chan string {\n\n\tlog.Printf(\"[INFO] custom: Using custom routes from %s\", b.cfg.Host)\n\tch := make(chan string, 1)\n\tgo customRoutes(b.cfg, ch)\n\treturn ch\n}\n\nfunc (b *be) WatchManual() chan string {\n\treturn make(chan string)\n}\n\nfunc (b *be) WatchNoRouteHTML() chan string {\n\tch := make(chan string, 1)\n\tch <- b.cfg.NoRouteHTML\n\treturn ch\n}\n"
  },
  {
    "path": "registry/custom/custom.go",
    "content": "package custom\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/route\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n)\n\nfunc customRoutes(cfg *config.Custom, ch chan string) {\n\n\tvar Routes *[]route.RouteDef\n\tvar trans *http.Transport\n\tvar URL string\n\n\tif !cfg.CheckTLSSkipVerify {\n\t\ttrans = &http.Transport{}\n\n\t} else {\n\t\ttrans = &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: trans,\n\t\tTimeout:   cfg.Timeout,\n\t}\n\n\tif cfg.QueryParams != \"\" {\n\t\tURL = fmt.Sprintf(\"%s://%s/%s?%s\", cfg.Scheme, cfg.Host, cfg.Path, cfg.QueryParams)\n\t} else {\n\t\tURL = fmt.Sprintf(\"%s://%s/%s\", cfg.Scheme, cfg.Host, cfg.Path)\n\t}\n\n\treq, err := http.NewRequest(\"GET\", URL, nil)\n\tif err != nil {\n\t\tlog.Printf(\"[ERROR] Can not generate new HTTP request\")\n\t}\n\treq.Close = true\n\n\tfor {\n\t\tfunc() {\n\t\t\tlog.Printf(\"[DEBUG] Custom Registry starting request %s \\n\", time.Now())\n\t\t\tresp, err := client.Do(req)\n\t\t\tif resp != nil {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif err := resp.Body.Close(); err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error Can not close HTTP resp body - %s -%s \\n\", URL, err.Error())\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tch <- fmt.Sprintf(\"Error Sending HTTPs Request To Custom be - %s -%s\", URL, err.Error())\n\t\t\t\ttime.Sleep(cfg.PollInterval)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif resp.StatusCode != 200 {\n\t\t\t\tch <- fmt.Sprintf(\"Error Non-200 return (%v) from  -%s\", resp.StatusCode, URL)\n\t\t\t\ttime.Sleep(cfg.PollInterval)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlog.Printf(\"[DEBUG] Custom Registry begin decoding json %s \\n\", time.Now())\n\t\t\tdecoder := json.NewDecoder(resp.Body)\n\t\t\terr = decoder.Decode(&Routes)\n\t\t\tif err != nil {\n\t\t\t\tch <- fmt.Sprintf(\"Error decoding request - %s -%s\", URL, err.Error())\n\t\t\t\ttime.Sleep(cfg.PollInterval)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Printf(\"[DEBUG] Custom Registry building table %s \\n\", time.Now())\n\t\t\tt, err := route.NewTableCustom(Routes)\n\t\t\tif err != nil {\n\t\t\t\tch <- fmt.Sprintf(\"Error generating new table - %s\", err.Error())\n\t\t\t}\n\t\t\tlog.Printf(\"[DEBUG] Custom Registry building table complete %s \\n\", time.Now())\n\t\t\troute.SetTable(t)\n\t\t\tlog.Printf(\"[DEBUG] Custom Registry table set complete %s \\n\", time.Now())\n\t\t\tch <- \"OK\"\n\t\t\ttime.Sleep(cfg.PollInterval)\n\t\t}()\n\t}\n\n}\n"
  },
  {
    "path": "registry/custom/custom_test.go",
    "content": "package custom\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/route\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestCustomRoutes(t *testing.T) {\n\n\tvar resp string\n\tcfg := config.Custom{\n\t\tHost:               \"localhost:8080\",\n\t\tPath:               \"test\",\n\t\tScheme:             \"http\",\n\t\tCheckTLSSkipVerify: false,\n\t\tPollInterval:       3 * time.Second,\n\t\tTimeout:            3 * time.Second,\n\t}\n\n\tch := make(chan string, 1)\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/test\", handleTest)\n\tserver := &http.Server{\n\t\tAddr:    \"localhost:8080\",\n\t\tHandler: mux,\n\t}\n\tgo server.ListenAndServe()\n\ttime.Sleep(3 * time.Second)\n\tdefer server.Close()\n\n\tgo customRoutes(&cfg, ch)\n\n\tresp = <-ch\n\n\tif resp != \"OK\" {\n\t\tfmt.Printf(\"Failed to get routes for custom backend - %s\", resp)\n\t\tt.FailNow()\n\t}\n}\n\nfunc handleTest(w http.ResponseWriter, r *http.Request) {\n\n\tvar routes []route.RouteDef\n\tvar tags = []string{\"tag1\", \"tag2\"}\n\tvar opts = make(map[string]string)\n\topts[\"tlsskipverify\"] = \"true\"\n\topts[\"proto\"] = \"http\"\n\n\tvar route1 = route.RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service1\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.1:8080\",\n\t\tWeight:  0.50,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\n\tvar route2 = route.RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service1\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.2:8080\",\n\t\tWeight:  0.50,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\tvar route3 = route.RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service2\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.3:8080\",\n\t\tWeight:  0.25,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\n\troutes = append(routes, route1)\n\troutes = append(routes, route2)\n\troutes = append(routes, route3)\n\n\trt, _ := json.Marshal(routes)\n\n\tw.Write(rt)\n\n}\n"
  },
  {
    "path": "registry/file/backend.go",
    "content": "// Package file implements a simple file based registry\n// backend which reads the routes from a file once.\npackage file\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/registry\"\n\t\"github.com/fabiolb/fabio/registry/static\"\n)\n\nfunc NewBackend(cfg *config.File) (registry.Backend, error) {\n\troutes, err := os.ReadFile(cfg.RoutesPath)\n\tif err != nil {\n\t\tlog.Println(\"[ERROR] Cannot read routes from \", cfg.RoutesPath)\n\t\treturn nil, err\n\t}\n\tnoroutehtml, err := os.ReadFile(cfg.NoRouteHTMLPath)\n\tif err != nil {\n\t\tlog.Println(\"[ERROR] Cannot read no route HTML from \", cfg.NoRouteHTMLPath)\n\t\treturn nil, err\n\t}\n\tstaticCfg := &config.Static{\n\t\tNoRouteHTML: string(noroutehtml),\n\t\tRoutes:      string(routes),\n\t}\n\treturn static.NewBackend(staticCfg)\n}\n"
  },
  {
    "path": "registry/static/backend.go",
    "content": "// Package static implements a simple static registry\n// backend which uses statically configured routes.\npackage static\n\nimport (\n\t\"github.com/fabiolb/fabio/config\"\n\t\"github.com/fabiolb/fabio/registry\"\n)\n\ntype be struct {\n\tcfg *config.Static\n}\n\nfunc NewBackend(cfg *config.Static) (registry.Backend, error) {\n\treturn &be{cfg}, nil\n}\n\nfunc (b *be) Register(services []string) error {\n\treturn nil\n}\n\nfunc (b *be) Deregister(serviceName string) error {\n\treturn nil\n}\n\nfunc (b *be) DeregisterAll() error {\n\treturn nil\n}\n\nfunc (b *be) ManualPaths() ([]string, error) {\n\treturn nil, nil\n}\n\nfunc (b *be) ReadManual(string) (value string, version uint64, err error) {\n\treturn \"\", 0, nil\n}\n\nfunc (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) {\n\treturn false, nil\n}\n\nfunc (b *be) WatchServices() chan string {\n\tch := make(chan string, 1)\n\tch <- b.cfg.Routes\n\treturn ch\n}\n\nfunc (b *be) WatchManual() chan string {\n\treturn make(chan string)\n}\n\nfunc (b *be) WatchNoRouteHTML() chan string {\n\tch := make(chan string, 1)\n\tch <- b.cfg.NoRouteHTML\n\treturn ch\n}\n"
  },
  {
    "path": "rootwarn_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst interval = time.Hour\n\nconst warnInsecure = `\n\n\t************************************************************\n\tYou are running fabio as root with the '-insecure' flag\n\tPlease check https://fabiolb.net/faq/binding-to-low-ports/\n\tfor alternatives.\n\t************************************************************\n\n`\n\nconst warn17behavior = `\n\n\t************************************************************\n\tYou are running fabio as root without the '-insecure' flag\n\tThis will stop working with fabio 1.7!\n\t************************************************************\n\n`\n\nvar once sync.Once\n\nfunc WarnIfRunAsRoot(allowRoot bool) {\n\t// todo(fs): should we emit the same warning when running inside a container?\n\t// todo(fs): check for existence of `/.dockerenv` to determine Docker environment.\n\tisRoot := os.Getuid() == 0\n\tif !isRoot {\n\t\treturn\n\t}\n\tdoWarn(allowRoot)\n\tonce.Do(func() { go remind(allowRoot) })\n}\n\nfunc doWarn(allowRoot bool) {\n\twarn := warnInsecure\n\tif !allowRoot {\n\t\twarn = warn17behavior\n\t}\n\tlog.Printf(\"[INFO] Running fabio as UID=%d EUID=%d GID=%d\", os.Getuid(), os.Geteuid(), os.Getgid())\n\tlog.Print(\"[WARN] \", warn)\n}\n\nfunc remind(allowRoot bool) {\n\tfor {\n\t\tdoWarn(allowRoot)\n\t\ttime.Sleep(interval)\n\t}\n}\n"
  },
  {
    "path": "rootwarn_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage main\n\nfunc WarnIfRunAsRoot(allowRoot bool) {\n\t// windows not supported\n}\n"
  },
  {
    "path": "route/access_rules.go",
    "content": "package route\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nconst (\n\tipAllowTag = \"allow:ip\"\n\tipDenyTag  = \"deny:ip\"\n)\n\n// AccessDeniedHTTP checks rules on the target for HTTP proxy routes.\nfunc (t *Target) AccessDeniedHTTP(r *http.Request) bool {\n\t// No rules ... skip checks\n\tif len(t.accessRules) == 0 {\n\t\treturn false\n\t}\n\n\thost, _, err := net.SplitHostPort(r.RemoteAddr)\n\tif err != nil {\n\t\tlog.Printf(\"[ERROR] failed to get host from remote header %s: %s\",\n\t\t\tr.RemoteAddr, err.Error())\n\t\treturn false\n\t}\n\n\tip := net.ParseIP(host)\n\tif ip == nil {\n\t\tlog.Printf(\"[WARN] failed to parse remote address %s\", host)\n\t}\n\n\t// check remote source and return if denied\n\tif t.denyByIP(ip) {\n\t\treturn true\n\t}\n\n\t// check xff source if present\n\tif xff := r.Header.Get(\"X-Forwarded-For\"); xff != \"\" {\n\t\t// Trusting XFF headers sent from clients is dangerous and generally\n\t\t// bad practice.  Therefore, we cannot assume which if any of the elements\n\t\t// is the actual client address.  To try and avoid the chance of spoofed\n\t\t// headers and/or loose upstream proxies we validate all elements in the header.\n\t\t// Specifically AWS does not strip XFF from anonymous internet sources:\n\t\t// https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-for\n\t\t// See lengthy github discussion for more background: https://github.com/fabiolb/fabio/pull/449\n\t\tfor _, xip := range strings.Split(xff, \",\") {\n\t\t\txip = strings.TrimSpace(xip)\n\t\t\tif xip == host {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif ip = net.ParseIP(xip); ip == nil {\n\t\t\t\tlog.Printf(\"[WARN] failed to parse xff address %s\", xip)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif t.denyByIP(ip) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\t// default allow\n\treturn false\n}\n\n// AccessDeniedTCP checks rules on the target for TCP proxy routes.\nfunc (t *Target) AccessDeniedTCP(c net.Conn) bool {\n\t// Calling RemoteAddr on a proxy-protocol enabled connection may block.\n\t// Therefore we explicitly check and bail out early if there are no\n\t// rules defined for the target.\n\t// See https://github.com/fabiolb/fabio/issues/524 for background.\n\tif len(t.accessRules) == 0 {\n\t\treturn false\n\t}\n\t// get remote address and validate assertion\n\taddr, ok := c.RemoteAddr().(*net.TCPAddr)\n\tif !ok {\n\t\tlog.Printf(\"[ERROR] failed to assert remote connection address for %s\", t.Service)\n\t\treturn false\n\t}\n\t// check remote connection address\n\tif t.denyByIP(addr.IP) {\n\t\treturn true\n\t}\n\t// default allow\n\treturn false\n}\n\nfunc (t *Target) denyByIP(ip net.IP) bool {\n\tif ip == nil || len(t.accessRules) == 0 {\n\t\treturn false\n\t}\n\t// check allow (whitelist) first if it exists\n\tif _, ok := t.accessRules[ipAllowTag]; ok {\n\t\tvar block *net.IPNet\n\t\tfor _, x := range t.accessRules[ipAllowTag] {\n\t\t\tif block, ok = x.(*net.IPNet); !ok {\n\t\t\t\tlog.Printf(\"[ERROR] failed to assert ip block while checking allow rule for %s\", t.Service)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// debug logging\n\t\t\tlog.Printf(\"[DEBUG] checking %s against ip allow rule %s\", ip.String(), block.String())\n\t\t\t// check block\n\t\t\tif block.Contains(ip) {\n\t\t\t\t// debug logging\n\t\t\t\tlog.Printf(\"[DEBUG] allowing request from %s via %s\", ip.String(), block.String())\n\t\t\t\t// specific allow matched - allow this request\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\t// we checked all the blocks - deny this request\n\t\tlog.Printf(\"[INFO] route rules denied access from %s to %s\",\n\t\t\tip.String(), t.URL.String())\n\t\treturn true\n\t}\n\n\t// still going - check deny (blacklist) if it exists\n\tif _, ok := t.accessRules[ipDenyTag]; ok {\n\t\tvar block *net.IPNet\n\t\tfor _, x := range t.accessRules[ipDenyTag] {\n\t\t\tif block, ok = x.(*net.IPNet); !ok {\n\t\t\t\tlog.Printf(\"[INFO] failed to assert ip block while checking deny rule for %s\", t.Service)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// debug logging\n\t\t\tlog.Printf(\"[DEBUG] checking %s against ip deny rule %s\", ip.String(), block.String())\n\t\t\t// check block\n\t\t\tif block.Contains(ip) {\n\t\t\t\t// specific deny matched - deny this request\n\t\t\t\tlog.Printf(\"[INFO] route rules denied access from %s to %s\",\n\t\t\t\t\tip.String(), t.URL.String())\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\t// debug logging\n\tlog.Printf(\"[DEBUG] default allowing request from %s that was not denied\", ip.String())\n\n\t// default - do not deny\n\treturn false\n}\n\n// ProcessAccessRules processes access rules from options specified on the target route\nfunc (t *Target) ProcessAccessRules() error {\n\tif t.Opts[\"allow\"] != \"\" && t.Opts[\"deny\"] != \"\" {\n\t\treturn errors.New(\"specifying allow and deny on the same route is not supported\")\n\t}\n\n\tfor _, allowDeny := range []string{\"allow\", \"deny\"} {\n\t\tif t.Opts[allowDeny] != \"\" {\n\t\t\tif err := t.parseAccessRule(allowDeny); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (t *Target) parseAccessRule(allowDeny string) error {\n\tvar accessTag string\n\tvar temps []string\n\tvar value string\n\tvar ip net.IP\n\n\t// init rules if needed\n\tif t.accessRules == nil {\n\t\tt.accessRules = make(map[string][]interface{})\n\t}\n\n\t// loop over rule elements\n\tfor _, c := range strings.Split(t.Opts[allowDeny], \",\") {\n\t\tif temps = strings.SplitN(c, \":\", 2); len(temps) != 2 {\n\t\t\treturn fmt.Errorf(\"invalid access item, expected <type>:<data>, got %s\", temps)\n\t\t}\n\n\t\t// form access type tag\n\t\taccessTag = allowDeny + \":\" + strings.ToLower(strings.TrimSpace(temps[0]))\n\n\t\t// switch on formed access tag - currently only ip types are implemented\n\t\tswitch accessTag {\n\t\tcase ipAllowTag, ipDenyTag:\n\t\t\tif value = strings.TrimSpace(temps[1]); !strings.Contains(value, \"/\") {\n\t\t\t\tif ip = net.ParseIP(value); ip == nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to parse IP %s\", value)\n\t\t\t\t}\n\t\t\t\tif ip.To4() != nil {\n\t\t\t\t\tvalue = ip.String() + \"/32\"\n\t\t\t\t} else {\n\t\t\t\t\tvalue = ip.String() + \"/128\"\n\t\t\t\t}\n\t\t\t}\n\t\t\t_, net, err := net.ParseCIDR(value)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to parse CIDR %s with error: %s\",\n\t\t\t\t\tc, err.Error())\n\t\t\t}\n\t\t\t// add element to rule map\n\t\t\tt.accessRules[accessTag] = append(t.accessRules[accessTag], net)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unknown access item type: %s\", temps[0])\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "route/access_rules_test.go",
    "content": "package route\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestAccessRules_parseAccessRule(t *testing.T) {\n\ttests := []struct {\n\t\tdesc      string\n\t\tallowDeny string\n\t\tfail      bool\n\t}{\n\t\t{\n\t\t\tdesc:      \"valid ipv4 rule\",\n\t\t\tallowDeny: \"ip:10.0.0.0/8,ip:192.168.0.0/24,ip:1.2.3.4/32\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"valid ipv6 rule\",\n\t\t\tallowDeny: \"ip:1234:567:beef:cafe::/64,ip:1234:5678:dead:beef::/32\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"invalid rule type\",\n\t\t\tallowDeny: \"xxx:10.0.0.0/8\",\n\t\t\tfail:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"ip rule with incomplete address\",\n\t\t\tallowDeny: \"ip:10/8\",\n\t\t\tfail:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"ip rule with bad cidr mask\",\n\t\t\tallowDeny: \"ip:10.0.0.0/255\",\n\t\t\tfail:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"single ipv4 with no mask\",\n\t\t\tallowDeny: \"ip:1.2.3.4\",\n\t\t\tfail:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"single ipv6 with no mask\",\n\t\t\tallowDeny: \"ip:fe80::1\",\n\t\t\tfail:      false,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tfor _, ad := range []string{\"allow\", \"deny\"} {\n\t\t\t\ttgt := &Target{Opts: map[string]string{ad: tt.allowDeny}}\n\t\t\t\terr := tgt.parseAccessRule(ad)\n\t\t\t\tif err != nil && !tt.fail {\n\t\t\t\t\tt.Errorf(\"%d: %s\\nfailed: %s\", i, tt.desc, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAccessRules_denyByIP(t *testing.T) {\n\ttests := []struct {\n\t\tdesc   string\n\t\ttarget *Target\n\t\tremote net.IP\n\t\tdenied bool\n\t}{\n\t\t{\n\t\t\tdesc: \"allow rule with included ipv4\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"10.10.0.1\"),\n\t\t\tdenied: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"allow rule with exluded ipv4\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1.2.3.4\"),\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"deny rule with included ipv4\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"deny\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"10.10.0.1\"),\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"deny rule with excluded ipv4\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"deny\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1.2.3.4\"),\n\t\t\tdenied: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"allow rule with included ipv6\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:1234:dead:beef:cafe::/64\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1234:dead:beef:cafe::5678\"),\n\t\t\tdenied: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"allow rule with exluded ipv6\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:1234:dead:beef:cafe::/64\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1234:5678::1\"),\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"deny rule with included ipv6\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"deny\": \"ip:1234:dead:beef:cafe::/64\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1234:dead:beef:cafe::5678\"),\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"deny rule with excluded ipv6\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"deny\": \"ip:1234:dead:beef:cafe::/64\"},\n\t\t\t},\n\t\t\tremote: net.ParseIP(\"1234:5678::1\"),\n\t\t\tdenied: false,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tif err := tt.target.ProcessAccessRules(); err != nil {\n\t\t\t\tt.Errorf(\"%d: %s - failed to process access rules: %s\",\n\t\t\t\t\ti, tt.desc, err.Error())\n\t\t\t}\n\t\t\ttt.target.URL, _ = url.Parse(\"http://testing.test/\")\n\t\t\tif deny := tt.target.denyByIP(tt.remote); deny != tt.denied {\n\t\t\t\tt.Errorf(\"%d: %s\\ngot denied: %t\\nwant denied: %t\\n\",\n\t\t\t\t\ti, tt.desc, deny, tt.denied)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAccessRules_AccessDeniedHTTP(t *testing.T) {\n\treq, _ := http.NewRequest(\"GET\", \"http://example.com/\", nil)\n\ttests := []struct {\n\t\tdesc   string\n\t\ttarget *Target\n\t\txff    string\n\t\tremote string\n\t\tdenied bool\n\t}{\n\t\t{\n\t\t\tdesc: \"single denied xff and allowed remote addr\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\txff:    \"10.11.12.13, 1.1.1.2, 10.11.12.14\",\n\t\t\tremote: \"10.11.12.1:65500\",\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"allowed xff and denied remote addr\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\txff:    \"10.11.12.13, 1.2.3.4\",\n\t\t\tremote: \"1.1.1.2:65500\",\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"single allowed xff and allowed remote addr\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\txff:    \"10.11.12.13, 1.2.3.4\",\n\t\t\tremote: \"192.168.0.12:65500\",\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"denied xff and denied remote addr\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\txff:    \"1.2.3.4, 10.11.12.13, 10.11.12.14\",\n\t\t\tremote: \"200.17.18.20:65500\",\n\t\t\tdenied: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"all allowed xff and allowed remote addr\",\n\t\t\ttarget: &Target{\n\t\t\t\tOpts: map[string]string{\"allow\": \"ip:10.0.0.0/8,ip:192.168.0.0/24\"},\n\t\t\t},\n\t\t\txff:    \"10.11.12.13, 10.110.120.130\",\n\t\t\tremote: \"192.168.0.12:65500\",\n\t\t\tdenied: false,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\ttt := tt // capture loop var\n\n\t\treq.Header = http.Header{\"X-Forwarded-For\": []string{tt.xff}}\n\t\treq.RemoteAddr = tt.remote\n\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tif err := tt.target.ProcessAccessRules(); err != nil {\n\t\t\t\tt.Errorf(\"%d: %s - failed to process access rules: %s\",\n\t\t\t\t\ti, tt.desc, err.Error())\n\t\t\t}\n\t\t\ttt.target.URL = mustParse(\"http://testing.test/\")\n\t\t\tif deny := tt.target.AccessDeniedHTTP(req); deny != tt.denied {\n\t\t\t\tt.Errorf(\"%d: %s\\ngot denied: %t\\nwant denied: %t\\n\",\n\t\t\t\t\ti, tt.desc, deny, tt.denied)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "route/auth.go",
    "content": "package route\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/fabiolb/fabio/auth\"\n)\n\nfunc (t *Target) Authorized(r *http.Request, w http.ResponseWriter, authSchemes map[string]auth.AuthScheme) bool {\n\tif t.AuthScheme == \"\" {\n\t\treturn true\n\t}\n\n\tscheme := authSchemes[t.AuthScheme]\n\n\tif scheme == nil {\n\t\tlog.Printf(\"[ERROR] unknown auth scheme '%s'\\n\", t.AuthScheme)\n\t\treturn false\n\t}\n\n\treturn scheme.Authorized(r, w)\n}\n"
  },
  {
    "path": "route/auth_test.go",
    "content": "package route\n\nimport (\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/auth\"\n)\n\ntype testAuth struct {\n\tok bool\n}\n\nfunc (t *testAuth) Authorized(r *http.Request, w http.ResponseWriter) bool {\n\treturn t.ok\n}\n\ntype responseWriter struct {\n\theader  http.Header\n\tcode    int\n\twritten []byte\n}\n\nfunc (rw *responseWriter) Header() http.Header {\n\treturn rw.header\n}\n\nfunc (rw *responseWriter) Write(b []byte) (int, error) {\n\trw.written = append(rw.written, b...)\n\treturn len(rw.written), nil\n}\n\nfunc (rw *responseWriter) WriteHeader(statusCode int) {\n\trw.code = statusCode\n}\n\nfunc TestTarget_Authorized(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tauthScheme  string\n\t\tauthSchemes map[string]auth.AuthScheme\n\t\tout         bool\n\t}{\n\t\t{\n\t\t\tname:       \"matches correct auth scheme\",\n\t\t\tauthScheme: \"mybasic\",\n\t\t\tauthSchemes: map[string]auth.AuthScheme{\n\t\t\t\t\"mybasic\": &testAuth{ok: true},\n\t\t\t},\n\t\t\tout: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"returns true when scheme is empty\",\n\t\t\tauthScheme: \"\",\n\t\t\tauthSchemes: map[string]auth.AuthScheme{\n\t\t\t\t\"mybasic\": &testAuth{ok: false},\n\t\t\t},\n\t\t\tout: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"returns false when scheme is unknown\",\n\t\t\tauthScheme: \"foobar\",\n\t\t\tauthSchemes: map[string]auth.AuthScheme{\n\t\t\t\t\"mybasic\": &testAuth{ok: true},\n\t\t\t},\n\t\t\tout: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttarget := &Target{\n\t\t\t\tAuthScheme: tt.authScheme,\n\t\t\t}\n\n\t\t\tif got, want := target.Authorized(&http.Request{}, &responseWriter{}, tt.authSchemes), tt.out; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "route/glob_cache.go",
    "content": "package route\n\nimport (\n\t\"github.com/gobwas/glob\"\n\t\"sync\"\n)\n\n// GlobCache implements an LRU cache for compiled glob patterns.\ntype GlobCache struct {\n\t// m maps patterns to compiled glob matchers.\n\tm sync.Map\n\n\t// l contains the added patterns and serves as an LRU cache.\n\t// l has a fixed size and is initialized in the constructor.\n\tl []string\n\n\t// h is the first element in l.\n\th int\n\n\t// n is the number of elements in l.\n\tn int\n}\n\nfunc NewGlobCache(size int) *GlobCache {\n\treturn &GlobCache{\n\t\tl: make([]string, size),\n\t}\n}\n\n// Get returns the compiled glob pattern if it compiled without\n// error. Otherwise, the function returns nil. If the pattern\n// is not in the cache it will be added.\nfunc (c *GlobCache) Get(pattern string) (glob.Glob, error) {\n\t// fast path\n\tif glb, ok := c.m.Load(pattern); ok {\n\t\t//Type Assert the returned interface{}\n\t\treturn glb.(glob.Glob), nil\n\t}\n\n\t// try to compile pattern\n\tglbCompiled, err := glob.Compile(pattern)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// if the LRU buffer is not full just append\n\t// the element to the buffer.\n\tif c.n < len(c.l) {\n\t\tc.m.Store(pattern, glbCompiled)\n\t\tc.l[c.n] = pattern\n\t\tc.n++\n\t\treturn glbCompiled, nil\n\t}\n\n\t// otherwise, remove the oldest element and move\n\t// the head. Note that once the buffer is full\n\t// (c.n == len(c.l)) it will never become smaller\n\t// again.\n\t// TODO add logging for cache full - How will this impact performance\n\tc.m.Delete(c.l[c.h])\n\tc.m.Store(pattern, glbCompiled)\n\tc.l[c.h] = pattern\n\tc.h = (c.h + 1) % c.n\n\treturn glbCompiled, nil\n}\n"
  },
  {
    "path": "route/glob_cache_test.go",
    "content": "package route\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n)\n\nfunc TestGlobCache(t *testing.T) {\n\tc := NewGlobCache(3)\n\n\tkeys := func() []string {\n\t\tvar kk []string\n\t\tc.m.Range(func(k, v interface{}) bool {\n\t\t\tkk = append(kk, k.(string))\n\t\t\treturn true\n\t\t})\n\t\tsort.Strings(kk)\n\t\treturn kk\n\t}\n\n\tc.Get(\"a\")\n\t// TODO  add back in when sync.Map supports Len function\n\t// TODO https://github.com/golang/go/issues/20680\n\t//if got, want := len(c.m), 1; got != want {\n\t//\tt.Fatalf(\"got len %d want %d\", got, want)\n\t//}\n\tif got, want := keys(), []string{\"a\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := c.l, []string{\"a\", \"\", \"\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\n\tc.Get(\"b\")\n\t// TODO  add back in when sync.Map supports Len function\n\t// TODO https://github.com/golang/go/issues/20680\n\t//if got, want := len(c.m), 2; got != want {\n\t//\tt.Fatalf(\"got len %d want %d\", got, want)\n\t//}\n\tif got, want := keys(), []string{\"a\", \"b\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := c.l, []string{\"a\", \"b\", \"\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\n\tc.Get(\"c\")\n\t// TODO  add back in when sync.Map supports Len function\n\t// TODO https://github.com/golang/go/issues/20680\n\t//if got, want := len(c.m), 3; got != want {\n\t//\tt.Fatalf(\"got len %d want %d\", got, want)\n\t//}\n\tif got, want := keys(), []string{\"a\", \"b\", \"c\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := c.l, []string{\"a\", \"b\", \"c\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\n\tc.Get(\"d\")\n\t// TODO  add back in when sync.Map supports Len function\n\t// TODO https://github.com/golang/go/issues/20680\n\t//if got, want := len(c.m), 3; got != want {\n\t//\tt.Fatalf(\"got len %d want %d\", got, want)\n\t//}\n\tif got, want := keys(), []string{\"b\", \"c\", \"d\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n\tif got, want := c.l, []string{\"d\", \"b\", \"c\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "route/issue57_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"testing\"\n)\n\n// TestIssue57 tests that after deleting a all targets for\n// a route requests to that route are handled by the next\n// matching route.\nfunc TestIssue57(t *testing.T) {\n\ttests := []string{\n\t\t`\n\t\troute add svca / http://foo.com:800\n\t\troute add svcb /foo http://foo.com:900\n\t\troute del svcb /foo http://foo.com:900`,\n\t\t`\n\t\troute add svca / http://foo.com:800\n\t \troute add svcb /foo http://foo.com:900\n\t \troute del svcb /foo`,\n\t\t`\n\t\troute add svca / http://foo.com:800\n\t \troute add svcb /foo http://foo.com:900\n\t \troute del svcb`,\n\t}\n\n\treq := &http.Request{URL: mustParse(\"/foo\")}\n\twant := \"http://foo.com:800\"\n\n\tfor i, tt := range tests {\n\t\ttbl, err := NewTable(bytes.NewBufferString(tt))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%d: got %v want nil\", i, err)\n\t\t}\n\t\ttarget := tbl.Lookup(req, rrPicker, prefixMatcher, globCache, globEnabled)\n\t\tif target == nil {\n\t\t\tt.Fatalf(\"%d: got %v want %v\", i, target, want)\n\t\t}\n\t\tif got := target.URL.String(); got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "route/matcher.go",
    "content": "package route\n\nimport (\n\t\"strings\"\n)\n\n// matcher determines whether a host/path matches a route\ntype matcher func(uri string, r *Route) bool\n\n// Matcher contains the available matcher functions.\n// Update config/load.go#load after updating.\nvar Matcher = map[string]matcher{\n\t\"prefix\":  prefixMatcher,\n\t\"glob\":    globMatcher,\n\t\"iprefix\": iPrefixMatcher,\n}\n\n// prefixMatcher matches path to the routes' path.\nfunc prefixMatcher(uri string, r *Route) bool {\n\treturn strings.HasPrefix(uri, r.Path)\n}\n\n// globMatcher matches path to the routes' path using gobwas/glob.\nfunc globMatcher(uri string, r *Route) bool {\n\treturn r.Glob.Match(uri)\n}\n\n// iPrefixMatcher matches path to the routes' path ignoring case\nfunc iPrefixMatcher(uri string, r *Route) bool {\n\t// todo(fs): if this turns out to be a performance issue we should cache\n\t// todo(fs): strings.ToLower(r.Path) in r.PathLower\n\tlowerURI := strings.ToLower(uri)\n\tlowerPath := strings.ToLower(r.Path)\n\treturn strings.HasPrefix(lowerURI, lowerPath)\n}\n"
  },
  {
    "path": "route/matcher_test.go",
    "content": "package route\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gobwas/glob\"\n)\n\nfunc TestPrefixMatcher(t *testing.T) {\n\ttests := []struct {\n\t\turi     string\n\t\tmatches bool\n\t\troute   *Route\n\t}{\n\t\t{uri: \"/foo\", matches: true, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/fools\", matches: true, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/fo\", matches: false, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/bar\", matches: false, route: &Route{Path: \"/foo\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.uri, func(t *testing.T) {\n\t\t\tif got, want := prefixMatcher(tt.uri, tt.route), tt.matches; got != want {\n\t\t\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGlobMatcher(t *testing.T) {\n\ttests := []struct {\n\t\turi     string\n\t\tmatches bool\n\t\troute   *Route\n\t}{\n\t\t// happy flows\n\t\t{uri: \"/foo\", matches: true, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/fool\", matches: true, route: &Route{Path: \"/foo?\"}},\n\t\t{uri: \"/fool\", matches: true, route: &Route{Path: \"/foo*\"}},\n\t\t{uri: \"/fools\", matches: true, route: &Route{Path: \"/foo*\"}},\n\t\t{uri: \"/fools\", matches: true, route: &Route{Path: \"/foo*\"}},\n\t\t{uri: \"/foo/x/bar\", matches: true, route: &Route{Path: \"/foo/*/bar\"}},\n\t\t{uri: \"/foo/x/y/z/w/bar\", matches: true, route: &Route{Path: \"/foo/**\"}},\n\t\t{uri: \"/foo/x/y/z/w/bar\", matches: true, route: &Route{Path: \"/foo/**/bar\"}},\n\n\t\t// error flows\n\t\t{uri: \"/fo\", matches: false, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/fools\", matches: false, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/fo\", matches: false, route: &Route{Path: \"/foo*\"}},\n\t\t{uri: \"/fools\", matches: false, route: &Route{Path: \"/foo.*\"}},\n\t\t{uri: \"/foo/x/y/z/w/baz\", matches: false, route: &Route{Path: \"/foo/**/bar\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.uri, func(t *testing.T) {\n\t\t\ttt.route.Glob = glob.MustCompile(tt.route.Path)\n\t\t\tif got, want := globMatcher(tt.uri, tt.route), tt.matches; got != want {\n\t\t\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIPrefixMatcher(t *testing.T) {\n\ttests := []struct {\n\t\turi     string\n\t\tmatches bool\n\t\troute   *Route\n\t}{\n\t\t{uri: \"/foo\", matches: false, route: &Route{Path: \"/fool\"}},\n\t\t{uri: \"/foo\", matches: true, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/Fool\", matches: true, route: &Route{Path: \"/foo\"}},\n\t\t{uri: \"/foo\", matches: true, route: &Route{Path: \"/Foo\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.uri, func(t *testing.T) {\n\t\t\tif got, want := iPrefixMatcher(tt.uri, tt.route), tt.matches; got != want {\n\t\t\t\tt.Fatalf(\"got %v want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "route/metrics_cleanup_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fabiolb/fabio/metrics\"\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\n// TestMetricsCleanup verifies that stale metrics are cleaned up when routes are removed\nfunc TestMetricsCleanup(t *testing.T) {\n\t// Create a custom prometheus registry for this test\n\treg := prometheus.NewRegistry()\n\n\t// Create histogram and counter vecs\n\thv := prometheus.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"fabio\",\n\t\tName:      \"route\",\n\t\tHelp:      \"test\",\n\t\tBuckets:   prometheus.DefBuckets,\n\t}, []string{\"service\", \"host\", \"path\", \"target\"})\n\treg.MustRegister(hv)\n\n\trxCv := prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\tNamespace: \"fabio\",\n\t\tName:      \"route_rx_total\",\n\t\tHelp:      \"test\",\n\t}, []string{\"service\", \"host\", \"path\", \"target\"})\n\treg.MustRegister(rxCv)\n\n\ttxCv := prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\tNamespace: \"fabio\",\n\t\tName:      \"route_tx_total\",\n\t\tHelp:      \"test\",\n\t}, []string{\"service\", \"host\", \"path\", \"target\"})\n\treg.MustRegister(txCv)\n\n\t// Create a test provider that wraps our vecs\n\tcounters.histogram = &testDeletableHistogram{hv: hv}\n\tcounters.rxCounter = &testDeletableCounter{cv: rxCv}\n\tcounters.txCounter = &testDeletableCounter{cv: txCv}\n\n\t// Create initial table with two services\n\tt1, err := NewTable(bytes.NewBufferString(`\nroute add svc-a /path-a http://target-a:8080/\nroute add svc-b /path-b http://target-b:8080/\n`))\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create table 1: %v\", err)\n\t}\n\n\t// Store it\n\tSetTable(t1)\n\n\t// Simulate traffic to create metric series\n\tfor _, routes := range t1 {\n\t\tfor _, r := range routes {\n\t\t\tfor _, target := range r.Targets {\n\t\t\t\ttarget.Timer.Observe(0.1)\n\t\t\t\ttarget.RxCounter.Add(100)\n\t\t\t\ttarget.TxCounter.Add(200)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check that metrics exist for both services\n\tmfs, _ := reg.Gather()\n\tt.Log(\"Metrics after initial traffic:\")\n\tsvcAFound, svcBFound := false, false\n\tfor _, mf := range mfs {\n\t\tfor _, m := range mf.GetMetric() {\n\t\t\tvar labels []string\n\t\t\tfor _, l := range m.GetLabel() {\n\t\t\t\tlabels = append(labels, l.GetName()+\"=\"+l.GetValue())\n\t\t\t\tif l.GetValue() == \"svc-a\" {\n\t\t\t\t\tsvcAFound = true\n\t\t\t\t}\n\t\t\t\tif l.GetValue() == \"svc-b\" {\n\t\t\t\t\tsvcBFound = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.Logf(\"  %s{%s}\", mf.GetName(), strings.Join(labels, \", \"))\n\t\t}\n\t}\n\n\tif !svcAFound {\n\t\tt.Error(\"svc-a metrics not found after initial traffic\")\n\t}\n\tif !svcBFound {\n\t\tt.Error(\"svc-b metrics not found after initial traffic\")\n\t}\n\n\t// Now create a new table WITHOUT svc-a\n\tt2, err := NewTable(bytes.NewBufferString(`\nroute add svc-b /path-b http://target-b:8080/\n`))\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create table 2: %v\", err)\n\t}\n\n\tt.Logf(\"Old table keys: %v\", collectTableMetricKeys(t1))\n\tt.Logf(\"New table keys: %v\", collectTableMetricKeys(t2))\n\n\t// Set the new table - this should trigger cleanup\n\tSetTable(t2)\n\n\t// Check that svc-a metrics are gone\n\tmfs, _ = reg.Gather()\n\tt.Log(\"Metrics after removing svc-a:\")\n\tsvcAFound, svcBFound = false, false\n\tfor _, mf := range mfs {\n\t\tfor _, m := range mf.GetMetric() {\n\t\t\tvar labels []string\n\t\t\tfor _, l := range m.GetLabel() {\n\t\t\t\tlabels = append(labels, l.GetName()+\"=\"+l.GetValue())\n\t\t\t\tif l.GetValue() == \"svc-a\" {\n\t\t\t\t\tsvcAFound = true\n\t\t\t\t}\n\t\t\t\tif l.GetValue() == \"svc-b\" {\n\t\t\t\t\tsvcBFound = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tt.Logf(\"  %s{%s}\", mf.GetName(), strings.Join(labels, \", \"))\n\t\t}\n\t}\n\n\tif svcAFound {\n\t\tt.Error(\"svc-a metrics should have been cleaned up but were still found\")\n\t}\n\tif !svcBFound {\n\t\tt.Error(\"svc-b metrics should still exist but were not found\")\n\t}\n}\n\n// testDeletableHistogram wraps HistogramVec for testing\ntype testDeletableHistogram struct {\n\thv  *prometheus.HistogramVec\n\tlvs []string\n}\n\nfunc (h *testDeletableHistogram) Observe(v float64) {\n\th.hv.WithLabelValues(extractValues(h.lvs)...).Observe(v)\n}\n\nfunc (h *testDeletableHistogram) With(labelValues ...string) gkm.Histogram {\n\treturn &testDeletableHistogram{\n\t\thv:  h.hv,\n\t\tlvs: append(append([]string{}, h.lvs...), labelValues...),\n\t}\n}\n\nfunc (h *testDeletableHistogram) DeleteLabelValues(labelValues ...string) bool {\n\treturn h.hv.DeleteLabelValues(labelValues...)\n}\n\n// testDeletableCounter wraps CounterVec for testing\ntype testDeletableCounter struct {\n\tcv  *prometheus.CounterVec\n\tlvs []string\n}\n\nfunc (c *testDeletableCounter) Add(v float64) {\n\tc.cv.WithLabelValues(extractValues(c.lvs)...).Add(v)\n}\n\nfunc (c *testDeletableCounter) With(labelValues ...string) gkm.Counter {\n\treturn &testDeletableCounter{\n\t\tcv:  c.cv,\n\t\tlvs: append(append([]string{}, c.lvs...), labelValues...),\n\t}\n}\n\nfunc (c *testDeletableCounter) DeleteLabelValues(labelValues ...string) bool {\n\treturn c.cv.DeleteLabelValues(labelValues...)\n}\n\n// extractValues extracts only values from alternating key-value pairs\nfunc extractValues(lvs []string) []string {\n\tvals := make([]string, 0, len(lvs)/2)\n\tfor i := 1; i < len(lvs); i += 2 {\n\t\tvals = append(vals, lvs[i])\n\t}\n\treturn vals\n}\n\n// Verify interfaces are satisfied\nvar _ metrics.DeletableHistogram = (*testDeletableHistogram)(nil)\nvar _ metrics.DeletableCounter = (*testDeletableCounter)(nil)\n\n// TestMetricsCleanupViaMultiProvider tests the cleanup through the MultiProvider\n// which is the actual code path used in production.\nfunc TestMetricsCleanupViaMultiProvider(t *testing.T) {\n\t// Create a custom prometheus registry for this test\n\treg := prometheus.NewRegistry()\n\n\t// Create a test provider that mimics the real PromProvider\n\ttestProv := &testPromProvider{reg: reg}\n\n\t// Wrap it in a MultiProvider like production does\n\tmultiProv := metrics.NewMultiProvider([]metrics.Provider{testProv})\n\n\t// Set up the route package to use this provider\n\tSetMetricsProvider(multiProv)\n\n\t// Verify that the histogram is a MultiHistogram\n\t_, ok := counters.histogram.(*metrics.MultiHistogram)\n\tif !ok {\n\t\tt.Fatalf(\"Expected MultiHistogram, got %T\", counters.histogram)\n\t}\n\n\t// Also verify it implements DeletableHistogram\n\t_, ok = counters.histogram.(metrics.DeletableHistogram)\n\tif !ok {\n\t\tt.Fatal(\"MultiHistogram should implement DeletableHistogram\")\n\t}\n\n\t// Create initial table with two services\n\tt1, err := NewTable(bytes.NewBufferString(`\nroute add svc-a /path-a http://target-a:8080/\nroute add svc-b /path-b http://target-b:8080/\n`))\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create table 1: %v\", err)\n\t}\n\tSetTable(t1)\n\n\t// Simulate traffic to create metric series\n\tfor _, routes := range t1 {\n\t\tfor _, r := range routes {\n\t\t\tfor _, target := range r.Targets {\n\t\t\t\ttarget.Timer.Observe(0.1)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Verify svc-a metrics exist\n\tmfs, _ := reg.Gather()\n\tsvcAFound := false\n\tfor _, mf := range mfs {\n\t\tfor _, m := range mf.GetMetric() {\n\t\t\tfor _, l := range m.GetLabel() {\n\t\t\t\tif l.GetValue() == \"svc-a\" {\n\t\t\t\t\tsvcAFound = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif !svcAFound {\n\t\tt.Error(\"svc-a metrics should exist after traffic\")\n\t}\n\n\t// Remove svc-a from the table\n\tt2, err := NewTable(bytes.NewBufferString(`\nroute add svc-b /path-b http://target-b:8080/\n`))\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create table 2: %v\", err)\n\t}\n\tSetTable(t2)\n\n\t// Verify svc-a metrics are gone\n\tmfs, _ = reg.Gather()\n\tsvcAFound = false\n\tfor _, mf := range mfs {\n\t\tfor _, m := range mf.GetMetric() {\n\t\t\tfor _, l := range m.GetLabel() {\n\t\t\t\tif l.GetValue() == \"svc-a\" {\n\t\t\t\t\tsvcAFound = true\n\t\t\t\t\tt.Logf(\"Found unexpected metric: %s with label %s=%s\", mf.GetName(), l.GetName(), l.GetValue())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif svcAFound {\n\t\tt.Error(\"svc-a metrics should have been cleaned up via MultiProvider\")\n\t}\n}\n\n// testPromProvider is a test provider that creates deletable metrics\ntype testPromProvider struct {\n\treg *prometheus.Registry\n}\n\nfunc (p *testPromProvider) NewCounter(name string, labels ...string) gkm.Counter {\n\tcv := prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\tNamespace: \"test\",\n\t\tName:      sanitizeName(name),\n\t}, labels)\n\tp.reg.MustRegister(cv)\n\treturn &testDeletableCounter{cv: cv}\n}\n\nfunc (p *testPromProvider) NewGauge(name string, labels ...string) gkm.Gauge {\n\tgv := prometheus.NewGaugeVec(prometheus.GaugeOpts{\n\t\tNamespace: \"test\",\n\t\tName:      sanitizeName(name),\n\t}, labels)\n\tp.reg.MustRegister(gv)\n\treturn &testDeletableGauge{gv: gv}\n}\n\nfunc (p *testPromProvider) NewHistogram(name string, labels ...string) gkm.Histogram {\n\thv := prometheus.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"test\",\n\t\tName:      sanitizeName(name),\n\t\tBuckets:   prometheus.DefBuckets,\n\t}, labels)\n\tp.reg.MustRegister(hv)\n\treturn &testDeletableHistogram{hv: hv}\n}\n\n// sanitizeName replaces dots with underscores for prometheus metric names\nfunc sanitizeName(name string) string {\n\treturn strings.ReplaceAll(name, \".\", \"_\")\n}\n\n// testDeletableGauge for completeness\ntype testDeletableGauge struct {\n\tgv  *prometheus.GaugeVec\n\tlvs []string\n}\n\nfunc (g *testDeletableGauge) Set(v float64) {\n\tg.gv.WithLabelValues(extractValues(g.lvs)...).Set(v)\n}\n\nfunc (g *testDeletableGauge) Add(v float64) {\n\tg.gv.WithLabelValues(extractValues(g.lvs)...).Add(v)\n}\n\nfunc (g *testDeletableGauge) With(labelValues ...string) gkm.Gauge {\n\treturn &testDeletableGauge{\n\t\tgv:  g.gv,\n\t\tlvs: append(append([]string{}, g.lvs...), labelValues...),\n\t}\n}\n\nfunc (g *testDeletableGauge) DeleteLabelValues(labelValues ...string) bool {\n\treturn g.gv.DeleteLabelValues(labelValues...)\n}\n\nvar _ metrics.DeletableGauge = (*testDeletableGauge)(nil)\nvar _ metrics.Provider = (*testPromProvider)(nil)\n"
  },
  {
    "path": "route/parse_new.go",
    "content": "package route\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\treRouteAdd    = regexp.MustCompile(`^route\\s+add`)\n\treRouteDel    = regexp.MustCompile(`^route\\s+del`)\n\treRouteWeight = regexp.MustCompile(`^route\\s+weight`)\n\treComment     = regexp.MustCompile(`^(#|//)`)\n\treBlankLine   = regexp.MustCompile(`^\\s*$`)\n)\n\nconst Commands = `\nRoute commands can have the following form:\n\nroute add <svc> <src> <dst>[ weight <w>][ tags \"<t1>,<t2>,...\"][ opts \"k1=v1 k2=v2 ...\"]\n  - Add route for service svc from src to dst with optional weight, tags and options.\n    Valid options are:\n\n\t  strip=/path        : forward '/path/to/file' as '/to/file'\n\t  prepend=/prefix    : forward '/path/to/file' as '/prefix/path/to/file'\n\t  proto=tcp          : upstream service is TCP, dst is ':port'\n\t  proto=https        : upstream service is HTTPS\n\t  tlsskipverify=true : disable TLS cert validation for HTTPS upstream\n\t  host=name          : set the Host header to 'name'. If 'name == \"dst\"' then the 'Host' header will be set to the registered upstream host name\n\t  register=name      : register fabio as new service 'name'. Useful for registering hostnames for host specific routes.\n      auth=name          : name of the auth scheme to use (defined in proxy.auth)\n\nroute del <svc>[ <src>[ <dst>]]\n  - Remove route matching svc, src and/or dst\n\nroute del <svc> tags \"<t1>,<t2>,...\"\n  - Remove all routes of service matching svc and tags\n\nroute del tags \"<t1>,<t2>,...\"\n  - Remove all routes matching tags\n\nroute weight <svc> <src> weight <w> tags \"<t1>,<t2>,...\"\n  - Route w% of traffic to all services matching svc, src and tags\n\nroute weight <src> weight <w> tags \"<t1>,<t2>,...\"\n  - Route w% of traffic to all services matching src and tags\n\nroute weight <svc> <src> weight <w>\n  - Route w% of traffic to all services matching svc and src\n\nroute weight service host/path weight w tags \"tag1,tag2\"\n  - Route w% of traffic to all services matching service, host/path and tags\n\n    w is a float > 0 describing a percentage, e.g. 0.5 == 50%\n    w <= 0: means no fixed weighting. Traffic is evenly distributed\n    w > 0: route will receive n% of traffic. If sum(w) > 1 then w is normalized.\n    sum(w) >= 1: only matching services will receive traffic\n\n   Note that the total sum of traffic sent to all matching routes is w%.\n`\n\n// Parse loads a routing table from a set of route commands.\n//\n// The commands are parsed in order and order matters.\n// Deleting a route that has not been created yet yields\n// a different result than the other way around.\nfunc Parse(in *bytes.Buffer) (defs []*RouteDef, err error) {\n\tvar def *RouteDef\n\tvar i int\n\tscanner := bufio.NewScanner(in)\n\tfor scanner.Scan() {\n\t\tvar err error\n\t\tresult := strings.TrimSpace(scanner.Text())\n\t\ti++\n\t\tswitch {\n\t\tcase reComment.MatchString(result) || reBlankLine.MatchString(result):\n\t\t\tcontinue\n\t\tcase reRouteAdd.MatchString(result):\n\t\t\tdef, err = parseRouteAdd(result)\n\t\tcase reRouteDel.MatchString(result):\n\t\t\tdef, err = parseRouteDel(result)\n\t\tcase reRouteWeight.MatchString(result):\n\t\t\tdef, err = parseRouteWeight(result)\n\t\tdefault:\n\t\t\terr = errors.New(\"syntax error: 'route' expected\")\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"line %d: %s\", i, err)\n\t\t}\n\t\tdefs = append(defs, def)\n\t}\n\treturn defs, nil\n}\n\n// ParseAliases scans a set of route commands for the \"register\" option and\n// returns a list of services which should be registered by the backend.\nfunc ParseAliases(in string) (names []string, err error) {\n\tvar defs []*RouteDef\n\tvar def *RouteDef\n\tfor i, s := range strings.Split(in, \"\\n\") {\n\t\tvar err error\n\t\ts = strings.TrimSpace(s)\n\t\tswitch {\n\t\tcase reComment.MatchString(s) || reBlankLine.MatchString(s):\n\t\t\tcontinue\n\t\tcase reRouteAdd.MatchString(s):\n\t\t\tdef, err = parseRouteAdd(s)\n\t\tcase reRouteDel.MatchString(s):\n\t\t\tdef, err = parseRouteDel(s)\n\t\tcase reRouteWeight.MatchString(s):\n\t\t\tdef, err = parseRouteWeight(s)\n\t\tdefault:\n\t\t\terr = errors.New(\"syntax error: 'route' expected\")\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"line %d: %s\", i+1, err)\n\t\t}\n\t\tdefs = append(defs, def)\n\t}\n\n\tvar aliases []string\n\n\tfor _, d := range defs {\n\t\tregisterName, ok := d.Opts[\"register\"]\n\t\tif ok {\n\t\t\taliases = append(aliases, registerName)\n\t\t}\n\t}\n\treturn aliases, nil\n}\n\n// route add <svc> <src> <dst>[ weight <w>][ tags \"<t1>,<t2>,...\"][ opts \"k=v k=v ...\"]\n// 1: service 2: src 3: dst 4: weight expr 5: weight val 6: tags expr 7: tags val 8: opts expr 9: opts val\nvar reAdd = mustCompileWithFlexibleSpace(`^route add (\\S+) (\\S+) (\\S+)( weight (\\S+))?( tags \"([^\"]*)\")?( opts \"([^\"]*)\")?$`)\n\nfunc parseRouteAdd(s string) (*RouteDef, error) {\n\tif m := reAdd.FindStringSubmatch(s); m != nil {\n\t\tw, err := parseWeight(m[5])\n\t\treturn &RouteDef{\n\t\t\tCmd:     RouteAddCmd,\n\t\t\tService: m[1],\n\t\t\tSrc:     m[2],\n\t\t\tDst:     m[3],\n\t\t\tWeight:  w,\n\t\t\tTags:    parseTags(m[7]),\n\t\t\tOpts:    parseOpts(m[9]),\n\t\t}, err\n\t}\n\treturn nil, errors.New(\"syntax error: 'route add' invalid\")\n}\n\n// route del <svc>[ <src>][ <dst>]\n// 1: service 2: src expr 3: src 4: dst expr 5: dst\nvar reDel = mustCompileWithFlexibleSpace(`^route del (\\S+)( (\\S+)( (\\S+))?)?$`)\n\n// route del <svc> tags \"<t1>,<t2>,...\"\n// 1: service 2: tags\nvar reDelSvcTags = mustCompileWithFlexibleSpace(`^route del (\\S+) tags \"([^\"]*)\"$`)\n\n// route del tags \"<t1>,<t2>,...\"\n// 2: tags\nvar reDelTags = mustCompileWithFlexibleSpace(`^route del tags \"([^\"]*)\"$`)\n\nfunc parseRouteDel(s string) (*RouteDef, error) {\n\tif m := reDelSvcTags.FindStringSubmatch(s); m != nil {\n\t\treturn &RouteDef{Cmd: RouteDelCmd, Service: m[1], Tags: parseTags(m[2])}, nil\n\t}\n\tif m := reDelTags.FindStringSubmatch(s); m != nil {\n\t\treturn &RouteDef{Cmd: RouteDelCmd, Tags: parseTags(m[1])}, nil\n\t}\n\tif m := reDel.FindStringSubmatch(s); m != nil {\n\t\treturn &RouteDef{Cmd: RouteDelCmd, Service: m[1], Src: m[3], Dst: m[5]}, nil\n\t}\n\treturn nil, errors.New(\"syntax error: 'route del' invalid\")\n}\n\n// route weight <svc> <src> weight <w>[ tags \"<t1>,<t2>,...\"]\n// 1: service 2: src 3: weight val 4: tags expr 5: tags val\nvar reWeightSvc = mustCompileWithFlexibleSpace(`^route weight (\\S+) (\\S+) weight (\\S+)( tags \"([^\"]*)\")?$`)\n\n// route weight <src> weight <w> tags \"<t1>,<t2>,...\"\n// 1: src 2: weight val 3: tags val\nvar reWeightSrc = mustCompileWithFlexibleSpace(`^route weight (\\S+) weight (\\S+) tags \"([^\"]*)\"$`)\n\nfunc parseRouteWeight(s string) (*RouteDef, error) {\n\tif m := reWeightSvc.FindStringSubmatch(s); m != nil {\n\t\tw, err := parseWeight(m[3])\n\t\treturn &RouteDef{\n\t\t\tCmd:     RouteWeightCmd,\n\t\t\tService: m[1],\n\t\t\tSrc:     m[2],\n\t\t\tWeight:  w,\n\t\t\tTags:    parseTags(m[5]),\n\t\t}, err\n\t}\n\tif m := reWeightSrc.FindStringSubmatch(s); m != nil {\n\t\tw, err := parseWeight(m[2])\n\t\treturn &RouteDef{\n\t\t\tCmd:    RouteWeightCmd,\n\t\t\tSrc:    m[1],\n\t\t\tWeight: w,\n\t\t\tTags:   parseTags(m[3]),\n\t\t}, err\n\t}\n\treturn nil, errors.New(\"syntax error: 'route weight' invalid\")\n}\n\nfunc mustCompileWithFlexibleSpace(re string) *regexp.Regexp {\n\treturn regexp.MustCompile(strings.ReplaceAll(re, \" \", \"\\\\s+\"))\n}\n\nfunc parseWeight(s string) (float64, error) {\n\tif s == \"\" {\n\t\treturn 0, nil\n\t}\n\tf, err := strconv.ParseFloat(s, 64)\n\tif err != nil {\n\t\treturn 0.0, errors.New(\"syntax error: weight value invalid\")\n\t}\n\treturn f, nil\n}\n\nfunc parseTags(s string) []string {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\ttags := strings.Split(s, \",\")\n\tfor i, t := range tags {\n\t\ttags[i] = strings.TrimSpace(t)\n\t}\n\treturn tags\n}\n\nfunc parseOpts(s string) map[string]string {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\tm := make(map[string]string)\n\tfor _, f := range strings.Fields(s) {\n\t\tp := strings.SplitN(f, \"=\", 2)\n\t\tif len(p) == 1 {\n\t\t\tm[f] = \"\"\n\t\t} else {\n\t\t\tm[p[0]] = p[1]\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "route/parse_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\tin   string\n\t\tout  []*RouteDef\n\t\tfail bool\n\t}{\n\t\t// error flows\n\t\t{\"FailEmpty\", ``, nil, false},\n\t\t{\"FailNoRoute\", `bang`, nil, true},\n\t\t{\"FailRouteNoCmd\", `route x`, nil, true},\n\t\t{\"FailRouteAddNoService\", `route add`, nil, true},\n\t\t{\"FailRouteAddNoSrc\", `route add svc`, nil, true},\n\n\t\t// happy flows\n\t\t{\n\t\t\tdesc: \"RouteAddService\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddTCPService\",\n\t\t\tin:   `route add svc :1234 tcp://1.2.3.4:5678`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \":1234\", Dst: \"tcp://1.2.3.4:5678\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddGRPCService\",\n\t\t\tin:   `route add svc :1234 grpc://1.2.3.4:5678`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \":1234\", Dst: \"grpc://1.2.3.4:5678\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWeight\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ weight 1.2`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Weight: 1.2}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWeightTags\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ weight 1.2 tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Weight: 1.2, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWeightOpts\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ weight 1.2 opts \"foo=bar baz=bang blimp\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Weight: 1.2, Opts: map[string]string{\"foo\": \"bar\", \"baz\": \"bang\", \"blimp\": \"\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWeightTagsOpts\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ weight 1.2 tags \"a,b\" opts \"foo=bar baz=bang blimp\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Weight: 1.2, Tags: []string{\"a\", \"b\"}, Opts: map[string]string{\"foo\": \"bar\", \"baz\": \"bang\", \"blimp\": \"\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWeightTagsOptsMoreSpaces\",\n\t\t\tin:   ` route  add  svc  /prefix  http://1.2.3.4/  weight  1.2  tags  \" a , b \"  opts  \"foo=bar  baz=bang  blimp\" `,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Weight: 1.2, Tags: []string{\"a\", \"b\"}, Opts: map[string]string{\"foo\": \"bar\", \"baz\": \"bang\", \"blimp\": \"\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddTags\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddTagsOpts\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ tags \"a,b\" opts \"foo=bar baz=bang blimp\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Tags: []string{\"a\", \"b\"}, Opts: map[string]string{\"foo\": \"bar\", \"baz\": \"bang\", \"blimp\": \"\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddOpts\",\n\t\t\tin:   `route add svc /prefix http://1.2.3.4/ opts \"foo=bar baz=bang blimp\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteAddCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\", Opts: map[string]string{\"foo\": \"bar\", \"baz\": \"bang\", \"blimp\": \"\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelTags\",\n\t\t\tin:   `route del tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelTagsMoreSpaces\",\n\t\t\tin:   `route  del  tags  \" a , b \"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelService\",\n\t\t\tin:   `route del svc`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelServiceTags\",\n\t\t\tin:   `route del svc tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelServiceTagsMoreSpaces\",\n\t\t\tin:   `route  del  svc  tags  \" a , b \"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelServiceSrc\",\n\t\t\tin:   `route del svc /prefix`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Src: \"/prefix\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelTCPServiceSrc\",\n\t\t\tin:   `route del svc :1234`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Src: \":1234\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelServiceSrcDst\",\n\t\t\tin:   `route del svc /prefix http://1.2.3.4/`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelTCPServiceSrcDst\",\n\t\t\tin:   `route del svc :1234 tcp://1.2.3.4:5678`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Src: \":1234\", Dst: \"tcp://1.2.3.4:5678\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteDelServiceSrcDstMoreSpaces\",\n\t\t\tin:   ` route  del  svc  /prefix  http://1.2.3.4/ `,\n\t\t\tout:  []*RouteDef{{Cmd: RouteDelCmd, Service: \"svc\", Src: \"/prefix\", Dst: \"http://1.2.3.4/\"}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteWeightServiceSrc\",\n\t\t\tin:   `route weight svc /prefix weight 1.2`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteWeightCmd, Service: \"svc\", Src: \"/prefix\", Weight: 1.2}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteWeightServiceSrcTags\",\n\t\t\tin:   `route weight svc /prefix weight 1.2 tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteWeightCmd, Service: \"svc\", Src: \"/prefix\", Weight: 1.2, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteWeightServiceSrcTagsMoreSpaces\",\n\t\t\tin:   ` route  weight  svc  /prefix  weight  1.2  tags  \" a , b \" `,\n\t\t\tout:  []*RouteDef{{Cmd: RouteWeightCmd, Service: \"svc\", Src: \"/prefix\", Weight: 1.2, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteWeightSrcTags\",\n\t\t\tin:   `route weight /prefix weight 1.2 tags \"a,b\"`,\n\t\t\tout:  []*RouteDef{{Cmd: RouteWeightCmd, Src: \"/prefix\", Weight: 1.2, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteWeightSrcTagsMoreSpaces\",\n\t\t\tin:   ` route  weight  /prefix  weight  1.2  tags  \" a , b \" `,\n\t\t\tout:  []*RouteDef{{Cmd: RouteWeightCmd, Src: \"/prefix\", Weight: 1.2, Tags: []string{\"a\", \"b\"}}},\n\t\t},\n\t}\n\n\treSyntaxError := regexp.MustCompile(`syntax error`)\n\n\tderef := func(def []*RouteDef) (defs []RouteDef) {\n\t\tfor _, d := range def {\n\t\t\tdefs = append(defs, *d)\n\t\t}\n\t\treturn\n\t}\n\n\trun := func(in string, def []*RouteDef, fail bool, parseFn func(*bytes.Buffer) ([]*RouteDef, error)) {\n\t\tout, err := parseFn(bytes.NewBufferString(in))\n\t\tswitch {\n\t\tcase err == nil && fail:\n\t\t\tt.Errorf(\"got error nil want fail\")\n\t\t\treturn\n\t\tcase err != nil && !fail:\n\t\t\tt.Errorf(\"got error %v want nil\", err)\n\t\t\treturn\n\t\tcase err != nil:\n\t\t\tif !reSyntaxError.MatchString(err.Error()) {\n\t\t\t\tt.Errorf(\"got error %q want 'syntax error.*'\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif got, want := out, def; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"\\ngot  %#v\\nwant %#v\", deref(got), deref(want))\n\t\t}\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(\"Parse-\"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, Parse) })\n\t}\n}\n\nfunc TestParseAliases(t *testing.T) {\n\ttests := []struct {\n\t\tdesc string\n\t\tin   string\n\t\tout  []string\n\t\tfail bool\n\t}{\n\t\t// error flows\n\t\t{\"FailEmpty\", ``, nil, false},\n\t\t{\"FailNoRoute\", `bang`, nil, true},\n\t\t{\"FailRouteNoCmd\", `route x`, nil, true},\n\t\t{\"FailRouteAddNoService\", `route add`, nil, true},\n\t\t{\"FailRouteAddNoSrc\", `route add svc`, nil, true},\n\n\t\t// happy flows with and without aliases\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWithoutAlias\",\n\t\t\tin:   `route add alpha-be alpha/ http://1.2.3.4/ opts \"strip=/path prepend=/new proto=https\"`,\n\t\t\tout:  []string(nil),\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServiceWithAlias\",\n\t\t\tin:   `route add alpha-be alpha/ http://1.2.3.4/ opts \"strip=/path prepend=/new proto=https register=alpha\"`,\n\t\t\tout:  []string{\"alpha\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServicesWithoutAliases\",\n\t\t\tin: `route add alpha-be alpha/ http://1.2.3.4/ opts \"strip=/path prepend=/new proto=tcp\"\n\t\t\troute add bravo-be bravo/ http://1.2.3.5/\n\t\t\troute add charlie-be charlie/ http://1.2.3.6/ opts \"host=charlie\"`,\n\t\t\tout: []string(nil),\n\t\t},\n\t\t{\n\t\t\tdesc: \"RouteAddServicesWithAliases\",\n\t\t\tin: `route add alpha-be alpha/ http://1.2.3.4/ opts \"register=alpha\"\n\t\t\troute add bravo-be bravo/ http://1.2.3.5/ opts \"strip=/foo prepend=/new register=bravo\"\n\t\t\troute add charlie-be charlie/ http://1.2.3.5/ opts \"host=charlie proto=https\"\n\t\t\troute add delta-be delta/ http://1.2.3.5/ opts \"host=delta proto=https register=delta\"`,\n\t\t\tout: []string{\"alpha\", \"bravo\", \"delta\"},\n\t\t},\n\t}\n\n\treSyntaxError := regexp.MustCompile(`syntax error`)\n\n\trun := func(in string, aliases []string, fail bool, parseFn func(string) ([]string, error)) {\n\t\tout, err := parseFn(in)\n\t\tswitch {\n\t\tcase err == nil && fail:\n\t\t\tt.Errorf(\"got error nil want fail\")\n\t\t\treturn\n\t\tcase err != nil && !fail:\n\t\t\tt.Errorf(\"got error %v want nil\", err)\n\t\t\treturn\n\t\tcase err != nil:\n\t\t\tif !reSyntaxError.MatchString(err.Error()) {\n\t\t\t\tt.Errorf(\"got error %q want 'syntax error.*'\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif got, want := out, aliases; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"\\ngot  %#v\\nwant %#v\", got, want)\n\t\t}\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(\"ParseAliases-\"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, ParseAliases) })\n\t}\n}\n"
  },
  {
    "path": "route/picker.go",
    "content": "package route\n\nimport (\n\t\"math/rand\"\n\t\"sync/atomic\"\n)\n\n// picker selects a target from a list of targets.\ntype picker func(r *Route) *Target\n\n// Picker contains the available picker functions.\n// Update config/load.go#load after updating.\nvar Picker = map[string]picker{\n\t\"rnd\": rndPicker,\n\t\"rr\":  rrPicker,\n}\n\n// rndPicker picks a random target from the list of targets.\nfunc rndPicker(r *Route) *Target {\n\treturn r.wTargets[randIntn(len(r.wTargets))]\n}\n\n// rrPicker picks the next target from a list of targets using round-robin.\nfunc rrPicker(r *Route) *Target {\n\tu := r.wTargets[r.total%uint64(len(r.wTargets))]\n\tatomic.AddUint64(&r.total, 1)\n\treturn u\n}\n\n// as it turns out, math/rand's Intn is now way faster (4x) than the previous implementation using\n// time.UnixNano().  As a bonus, this actually works properly on 32 bit platforms.\nvar randIntn = func(n int) int {\n\tif n == 0 {\n\t\treturn 0\n\t}\n\treturn rand.Intn(n)\n}\n"
  },
  {
    "path": "route/picker_test.go",
    "content": "package route\n\nimport (\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tfooDotCom = mustParse(\"http://foo.com/\")\n\tbarDotCom = mustParse(\"http://bar.com/\")\n)\n\nfunc mustParse(rawurl string) *url.URL {\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n\nfunc TestRndPicker(t *testing.T) {\n\tr := &Route{Host: \"www.bar.com\", Path: \"/foo\"}\n\tr.addTarget(\"svc\", fooDotCom, 0, nil, nil)\n\tr.addTarget(\"svc\", barDotCom, 0, nil, nil)\n\n\ttests := []struct {\n\t\trnd       int\n\t\ttargetURL *url.URL\n\t}{\n\t\t{0, fooDotCom},\n\t\t{1, barDotCom},\n\t}\n\n\tprev := randIntn\n\tdefer func() { randIntn = prev }()\n\n\tfor i, tt := range tests {\n\t\trandIntn = func(int) int { return i }\n\t\tif got, want := rndPicker(r).URL, tt.targetURL; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestRRPicker(t *testing.T) {\n\tr := &Route{Host: \"www.bar.com\", Path: \"/foo\"}\n\tr.addTarget(\"svc\", fooDotCom, 0, nil, nil)\n\tr.addTarget(\"svc\", barDotCom, 0, nil, nil)\n\n\ttests := []*url.URL{fooDotCom, barDotCom, fooDotCom, barDotCom, fooDotCom, barDotCom}\n\n\tfor i, tt := range tests {\n\t\tif got, want := rrPicker(r).URL, tt; !reflect.DeepEqual(got, want) {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\n// This is an improved version of the previous UnixNano implementation\n// This one does not overflow on 32 bit platforms, it casts to int after\n// doing mod.  doing it before caused overflows.\nvar oldRandInt = func(n int) int {\n\tif n == 0 {\n\t\treturn 0\n\t}\n\treturn int(time.Now().UnixNano() / int64(time.Microsecond) % int64(n))\n}\n\nvar result int // prevent compiler optimization\nfunc BenchmarkOldRandIntn(b *testing.B) {\n\tvar r int // more shields against compiler optimization\n\tfor i := range b.N {\n\t\tr = oldRandInt(i)\n\t}\n\tresult = r\n}\nfunc BenchmarkMathRandIntn(b *testing.B) {\n\tvar r int // more shields against compiler optimization\n\tfor i := range b.N {\n\t\tr = randIntn(i)\n\t}\n\tresult = r\n}\n"
  },
  {
    "path": "route/route.go",
    "content": "package route\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"github.com/fabiolb/fabio/transport\"\n\t\"log\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gobwas/glob\"\n)\n\n// Route maps a path prefix to one or more target URLs.\n// routes can have a weight value which describes the\n// amount of traffic this route should get. You can specify\n// that a route should get a fixed percentage of the traffic\n// independent of how many instances are running.\ntype Route struct {\n\n\t// Glob represents compiled pattern.\n\tGlob glob.Glob\n\t// Host contains the host of the route.\n\t// not used for routing but for config generation\n\t// Table has a map with the host as key\n\t// for faster lookup and smaller search space.\n\tHost string\n\n\t// Path is the path prefix from a request uri\n\tPath string\n\n\t// Targets contains the list of URLs\n\tTargets []*Target\n\n\t// wTargets contains targets distributed according to their weight\n\twTargets []*Target\n\n\t// total contains the total number of requests for this route.\n\t// Used by the RRPicker\n\ttotal uint64\n}\n\nfunc (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float64, tags []string, opts map[string]string) {\n\tif fixedWeight < 0 {\n\t\tfixedWeight = 0\n\t}\n\n\t// de-dup existing target\n\tfor _, t := range r.Targets {\n\t\tif t.Service == service && t.URL.String() == targetURL.String() && t.FixedWeight == fixedWeight && reflect.DeepEqual(t.Tags, tags) {\n\t\t\treturn\n\t\t}\n\t}\n\n\tt := &Target{\n\t\tService:     service,\n\t\tTags:        tags,\n\t\tOpts:        opts,\n\t\tURL:         targetURL,\n\t\tFixedWeight: fixedWeight,\n\t\tTimer:       counters.histogram.With(\"service\", service, \"host\", r.Host, \"path\", r.Path, \"target\", targetURL.String()),\n\t\tRxCounter:   counters.rxCounter.With(\"service\", service, \"host\", r.Host, \"path\", r.Path, \"target\", targetURL.String()),\n\t\tTxCounter:   counters.txCounter.With(\"service\", service, \"host\", r.Host, \"path\", r.Path, \"target\", targetURL.String()),\n\t}\n\n\tvar err error\n\tif opts != nil {\n\n\t\tt.StripPath = opts[\"strip\"]\n\t\tt.PrependPath = opts[\"prepend\"]\n\t\tt.TLSSkipVerify = opts[\"tlsskipverify\"] == \"true\"\n\t\tt.Host = opts[\"host\"]\n\t\tt.ProxyProto = opts[\"pxyproto\"] == \"true\"\n\n\t\t// if Host is \"dst\", we don't need a special transport to override the sni because\n\t\t// this is already the default behavior.\n\t\tif t.Host != \"\" && t.Host != \"dst\" && (t.URL.Scheme == \"https\" || opts[\"proto\"] == \"https\") {\n\t\t\tt.Transport = transport.NewTransport(&tls.Config{ServerName: t.Host, InsecureSkipVerify: t.TLSSkipVerify})\n\t\t}\n\n\t\tif opts[\"redirect\"] != \"\" {\n\t\t\tt.RedirectCode, err = strconv.Atoi(opts[\"redirect\"])\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[ERROR] redirect status code should be numeric in 3xx range. Got: %s\", opts[\"redirect\"])\n\t\t\t} else if t.RedirectCode < 300 || t.RedirectCode > 399 {\n\t\t\t\tt.RedirectCode = 0\n\t\t\t\tlog.Printf(\"[ERROR] redirect status code should be in 3xx range. Got: %s\", opts[\"redirect\"])\n\t\t\t}\n\t\t}\n\n\t\tif err = t.ProcessAccessRules(); err != nil {\n\t\t\tlog.Printf(\"[ERROR] failed to process access rules: %s\",\n\t\t\t\terr.Error())\n\t\t}\n\n\t\tt.AuthScheme = opts[\"auth\"]\n\t}\n\n\tr.Targets = append(r.Targets, t)\n\tr.weighTargets()\n}\n\nfunc (r *Route) filter(skip func(t *Target) bool) {\n\tvar clone []*Target\n\tfor _, t := range r.Targets {\n\t\tif skip(t) {\n\t\t\tcontinue\n\t\t}\n\t\tclone = append(clone, t)\n\t}\n\tr.Targets = clone\n\tr.weighTargets()\n}\n\nfunc (r *Route) setWeight(service string, weight float64, tags []string) int {\n\tloop := func(w float64) int {\n\t\tn := 0\n\t\tfor _, t := range r.Targets {\n\t\t\tif service != \"\" && t.Service != service {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(tags) > 0 && !contains(t.Tags, tags) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tn++\n\t\t\tt.FixedWeight = w\n\t\t}\n\t\treturn n\n\t}\n\n\t// if we have multiple matching targets\n\t// then we need to distribute the total\n\t// weight across all of them since the rule\n\t// states to assign only that percentage\n\t// of traffic to all matching routes combined.\n\tn := loop(0)\n\tw := weight / float64(n)\n\tloop(w)\n\n\tif n > 0 {\n\t\tr.weighTargets()\n\t}\n\treturn n\n}\n\nfunc contains(src, dst []string) bool {\n\tfor _, d := range dst {\n\t\tfound := false\n\t\tfor _, s := range src {\n\t\t\tif s == d {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (r *Route) TargetConfig(t *Target, addWeight bool) string {\n\ts := fmt.Sprintf(\"route add %s %s %s\", t.Service, r.Host+r.Path, t.URL)\n\tif addWeight {\n\t\ts += fmt.Sprintf(\" weight %2.4f\", t.Weight)\n\t} else if t.FixedWeight > 0 {\n\t\ts += fmt.Sprintf(\" weight %.4f\", t.FixedWeight)\n\t}\n\tif len(t.Tags) > 0 {\n\t\ts += fmt.Sprintf(\" tags %q\", strings.Join(t.Tags, \",\"))\n\t}\n\tif len(t.Opts) > 0 {\n\t\tvar keys []string\n\t\tfor k := range t.Opts {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\tsort.Strings(keys)\n\n\t\tvar vals []string\n\t\tfor _, k := range keys {\n\t\t\tvals = append(vals, k+\"=\"+t.Opts[k])\n\t\t}\n\t\ts += fmt.Sprintf(\" opts \\\"%s\\\"\", strings.Join(vals, \" \"))\n\t}\n\treturn s\n}\n\n// config returns the route configuration in the config language.\n// with the weights specified by the user.\nfunc (r *Route) config(addWeight bool) []string {\n\tvar cfg []string\n\tfor _, t := range r.Targets {\n\t\tif t.Weight <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcfg = append(cfg, r.TargetConfig(t, addWeight))\n\t}\n\treturn cfg\n}\n\n// maxSlots defines the maximum number of slots on the ring for\n// weighted round-robin distribution for a single route. Consequently,\n// this then defines the maximum number of separate instances that can\n// serve a single route. maxSlots must be a power of ten.\nconst maxSlots = 1e4 // 10000\n\n// weighTargets computes the share of traffic each target receives based\n// on its weight and the weight of the other targets.\n//\n// Traffic is first distributed to targets with a fixed weight. If the sum of\n// all fixed weights exceeds 100% then they are normalized to 100%.\n//\n// Targets with a dynamic weight will receive an equal share of the remaining\n// traffic if there is any left.\nfunc (r *Route) weighTargets() {\n\t// how big is the fixed weighted traffic?\n\tvar nFixed int\n\tvar sumFixed float64\n\tfor _, t := range r.Targets {\n\t\tif t.FixedWeight > 0 {\n\t\t\tnFixed++\n\t\t\tsumFixed += t.FixedWeight\n\t\t}\n\t}\n\n\t// if there are no targets with fixed weight then each target simply gets\n\t// an equal amount of traffic\n\tif nFixed == 0 {\n\t\tw := 1.0 / float64(len(r.Targets))\n\t\tfor _, t := range r.Targets {\n\t\t\tt.Weight = w\n\t\t}\n\t\tr.wTargets = r.Targets\n\t\treturn\n\t}\n\n\t// normalize fixed weights up (sumFixed < 1) or down (sumFixed > 1)\n\tscale := 1.0\n\tif sumFixed > 1 || (nFixed == len(r.Targets) && sumFixed < 1) {\n\t\tscale = 1 / sumFixed\n\t}\n\n\t// compute the weight for the targets with dynamic weights\n\tdynamic := (1 - sumFixed) / float64(len(r.Targets)-nFixed)\n\tif dynamic < 0 {\n\t\tdynamic = 0\n\t}\n\n\t// assign the actual weight to each target\n\tfor _, t := range r.Targets {\n\t\tif t.FixedWeight > 0 {\n\t\t\tt.Weight = t.FixedWeight * scale\n\t\t} else {\n\t\t\tt.Weight = dynamic\n\t\t}\n\t}\n\n\t// distribute the targets on a ring suitable for weighted round-robin\n\t// distribution\n\t//\n\t// This is done in two steps:\n\t//\n\t// Step one determines the necessary ring size to distribute the targets\n\t// according to their weight with reasonable accuracy. For example, two\n\t// targets with 50% weight fit in a ring of size 2 whereas two targets with\n\t// 10% and 90% weight require a ring of size 10.\n\t//\n\t// To keep it simple we allocate 10000 slots which provides slots to all\n\t// targets with at least a weight of 0.01%. In addition, we guarantee that\n\t// every target with a weight > 0 gets at least one slot. The case where\n\t// all targets get an equal share of traffic is handled earlier so this is\n\t// for situations with some fixed weight.\n\t//\n\t// Step two distributes the targets onto the ring spacing them out evenly\n\t// so that iterating over the ring performs the weighted rr distribution.\n\t// For example, a 50/50 distribution on a ring of size 10 should be\n\t// 0101010101 instead of 0000011111.\n\t//\n\t// To ensure that targets with smaller weights are properly placed we place\n\t// them on the ring first by sorting the targets by slot count.\n\t//\n\t// TODO(fs): I assume that this is some sort of mathematical problem\n\t// (coloring, optimizing, ...) but I don't know which. Happy to make this\n\t// more formal, if possible.\n\t//\n\tslots := make(byN, len(r.Targets))\n\tusedSlots := 0\n\tfor i, t := range r.Targets {\n\t\tn := int(float64(maxSlots) * t.Weight)\n\t\tif n == 0 && t.Weight > 0 {\n\t\t\tn = 1\n\t\t}\n\t\tslots[i].i = i\n\t\tslots[i].n = n\n\t\tusedSlots += n\n\t}\n\n\tsort.Sort(slots)\n\ttargets := make([]*Target, usedSlots)\n\tfor _, s := range slots {\n\t\tif s.n <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tnext, step := 0, usedSlots/s.n\n\t\tfor range s.n {\n\t\t\t// find the next empty slot\n\t\t\tfor targets[next] != nil {\n\t\t\t\tnext = (next + 1) % usedSlots\n\t\t\t}\n\n\t\t\t// use slot and move to next one\n\t\t\ttargets[next] = r.Targets[s.i]\n\t\t\tnext = (next + step) % usedSlots\n\t\t}\n\t}\n\n\tr.wTargets = targets\n}\n\ntype byN []struct{ i, n int }\n\nfunc (r byN) Len() int           { return len(r) }\nfunc (r byN) Swap(i, j int)      { r[i], r[j] = r[j], r[i] }\nfunc (r byN) Less(i, j int) bool { return r[i].n < r[j].n }\n"
  },
  {
    "path": "route/route_bench_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n)\n\nvar (\n\tb5Routes   Table\n\tb10Routes  Table\n\tb100Routes Table\n\tb500Routes Table\n\n\tonce sync.Once\n)\n\n// initRoutes is used for lazy one time initialization of the test data for\n// the parallel benchmarks via once\nfunc initRoutes() {\n\tb5Routes = makeRoutes(1, 5, 1, 6)\n\tb10Routes = makeRoutes(1, 5, 2, 6)\n\tb100Routes = makeRoutes(10, 5, 2, 24)\n\tb500Routes = makeRoutes(10, 10, 5, 24)\n}\n\nfunc BenchmarkPrefixMatcherRndPicker5Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b5Routes, prefixMatcher, rndPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRRPicker5Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b5Routes, prefixMatcher, rrPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRndPicker10Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b10Routes, prefixMatcher, rndPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRRPicker10Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b10Routes, prefixMatcher, rrPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRndPicker100Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b100Routes, prefixMatcher, rndPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRRPicker100Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b100Routes, prefixMatcher, rrPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRndPicker500Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b500Routes, prefixMatcher, rndPicker, b) })\n}\n\nfunc BenchmarkPrefixMatcherRRPicker500Routes(b *testing.B) {\n\tonce.Do(initRoutes)\n\tb.ResetTimer()\n\tb.SetParallelism(3)\n\tb.RunParallel(func(b *testing.PB) { benchmarkGet(b500Routes, prefixMatcher, rrPicker, b) })\n}\n\n// makeRoutes builds a set of routes for a set of domains\n// and target urls. For each domain all paths up to depth\n// are constructed and all host/path combinations have the\n// same target URLs. The number of generated routes is\n// domains * paths * depth.\nfunc makeRoutes(domains, paths, depth, urls int) Table {\n\ts := \"\"\n\tfor i := range domains {\n\t\tprefix := fmt.Sprintf(\"www.host-%d.com/\", i)\n\t\tfor range paths {\n\t\t\tfor k := range depth {\n\t\t\t\tprefix += fmt.Sprintf(\"path-%d/\", k)\n\t\t\t\tfor range urls {\n\t\t\t\t\ts += fmt.Sprintf(\"route add svc %s http://host:12345/\\n\", prefix)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tt, err := NewTable(bytes.NewBufferString(s))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn t\n}\n\n// makeRequests builds a list of http.Request objects with an\n// additional path for benchmarking.\nfunc makeRequests(t Table) []*http.Request {\n\treqs := []*http.Request{}\n\tfor host, hr := range t {\n\t\tfor _, r := range hr {\n\t\t\treq := &http.Request{Host: host, RequestURI: r.Path + \"/some/additional/path\"}\n\t\t\treqs = append(reqs, req)\n\t\t}\n\t}\n\treturn reqs\n}\n\n// benchmarkGet runs the benchmark on the Table.Lookup() function with the\n// given matcher and picker functions.\nfunc benchmarkGet(t Table, match matcher, pick picker, pb *testing.PB) {\n\treqs := makeRequests(t)\n\tk, n := len(reqs), 0\n\t//Glob Matching True\n\tfor pb.Next() {\n\t\tt.Lookup(reqs[n%k], pick, match, globCache, globEnabled)\n\t\tn++\n\t}\n}\n"
  },
  {
    "path": "route/route_def.go",
    "content": "package route\n\ntype Cmd string\n\nconst (\n\tRouteAddCmd    Cmd = \"route add\"\n\tRouteDelCmd    Cmd = \"route del\"\n\tRouteWeightCmd Cmd = \"route weight\"\n)\n\ntype RouteDef struct {\n\tOpts    map[string]string `json:\"opts,omitempty\"`\n\tCmd     Cmd               `json:\"cmd\"`\n\tService string            `json:\"service\"`\n\tSrc     string            `json:\"src\"`\n\tDst     string            `json:\"dst\"`\n\tTags    []string          `json:\"tags,omitempty\"`\n\tWeight  float64           `json:\"weight\"`\n}\n"
  },
  {
    "path": "route/routes.go",
    "content": "package route\n\n// Routes stores a list of routes usually for a single host.\ntype Routes []*Route\n\n// find returns the route with the given path and returns nil if none was found.\nfunc (rt Routes) find(path string) *Route {\n\tfor _, r := range rt {\n\t\tif r.Path == path {\n\t\t\treturn r\n\t\t}\n\t}\n\treturn nil\n}\n\n// sort by path in reverse order (most to least specific)\nfunc (rt Routes) Len() int           { return len(rt) }\nfunc (rt Routes) Swap(i, j int)      { rt[i], rt[j] = rt[j], rt[i] }\nfunc (rt Routes) Less(i, j int) bool { return rt[j].Path < rt[i].Path }\n"
  },
  {
    "path": "route/table.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\tgkm \"github.com/go-kit/kit/metrics\"\n\n\t\"github.com/gobwas/glob\"\n\n\t\"github.com/fabiolb/fabio/metrics\"\n)\n\nvar errInvalidPrefix = errors.New(\"route: prefix must not be empty\")\nvar errInvalidTarget = errors.New(\"route: target must not be empty\")\nvar errNoMatch = errors.New(\"route: no target match\")\n\n// table stores the active routing table. Must never be nil.\nvar table atomic.Value\n\n// package global metrics bits\n\n// init initializes the routing table.\nfunc init() {\n\ttable.Store(make(Table))\n\tnp := metrics.DiscardProvider{}\n\tSetMetricsProvider(np)\n}\n\ntype metrix struct {\n\thistogram gkm.Histogram\n\trxCounter gkm.Counter\n\ttxCounter gkm.Counter\n}\n\nvar counters metrix\n\n// makeMetricKey creates a unique key for a target's metric labels.\nfunc makeMetricKey(service, host, path, target string) string {\n\treturn service + \"\\x00\" + host + \"\\x00\" + path + \"\\x00\" + target\n}\n\n// parseMetricKey extracts label values from a metric key.\nfunc parseMetricKey(key string) (service, host, path, target string) {\n\tparts := strings.Split(key, \"\\x00\")\n\tif len(parts) == 4 {\n\t\treturn parts[0], parts[1], parts[2], parts[3]\n\t}\n\treturn \"\", \"\", \"\", \"\"\n}\n\n// collectTableMetricKeys extracts all metric label keys from a routing table.\nfunc collectTableMetricKeys(t Table) map[string]struct{} {\n\tkeys := make(map[string]struct{})\n\tfor _, routes := range t {\n\t\tfor _, r := range routes {\n\t\t\tfor _, target := range r.Targets {\n\t\t\t\tkey := makeMetricKey(target.Service, r.Host, r.Path, target.URL.String())\n\t\t\t\tkeys[key] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\treturn keys\n}\n\n// cleanupStaleMetrics removes metric label combinations that are no longer active.\n// It compares the old table against the new table to find removed targets.\nfunc cleanupStaleMetrics(oldTable, newTable Table) {\n\toldKeys := collectTableMetricKeys(oldTable)\n\tnewKeys := collectTableMetricKeys(newTable)\n\n\tlog.Printf(\"[INFO] cleanupStaleMetrics: oldKeys=%d newKeys=%d\", len(oldKeys), len(newKeys))\n\n\t// Find and delete stale labels (in old but not in new)\n\tfor key := range oldKeys {\n\t\tif _, exists := newKeys[key]; !exists {\n\t\t\tservice, host, path, target := parseMetricKey(key)\n\t\t\tlog.Printf(\"[INFO] Cleaning up stale metrics for service=%s host=%s path=%s target=%s\", service, host, path, target)\n\t\t\t// Delete from each metric type if they support deletion\n\t\t\tif dh, ok := counters.histogram.(metrics.DeletableHistogram); ok {\n\t\t\t\tdeleted := dh.DeleteLabelValues(service, host, path, target)\n\t\t\t\tlog.Printf(\"[INFO] Histogram delete returned: %v\", deleted)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"[WARN] Histogram does not implement DeletableHistogram, type=%T\", counters.histogram)\n\t\t\t}\n\t\t\tif dc, ok := counters.rxCounter.(metrics.DeletableCounter); ok {\n\t\t\t\tdc.DeleteLabelValues(service, host, path, target)\n\t\t\t}\n\t\t\tif dc, ok := counters.txCounter.(metrics.DeletableCounter); ok {\n\t\t\t\tdc.DeleteLabelValues(service, host, path, target)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc SetMetricsProvider(p metrics.Provider) {\n\tcounters.histogram = p.NewHistogram(\"route\", \"service\", \"host\", \"path\", \"target\")\n\tcounters.rxCounter = p.NewCounter(\"route.rx\", \"service\", \"host\", \"path\", \"target\")\n\tcounters.txCounter = p.NewCounter(\"route.tx\", \"service\", \"host\", \"path\", \"target\")\n}\n\n// GetTable returns the active routing table. The function\n// is safe to be called from multiple goroutines and the\n// value is never nil.\nfunc GetTable() Table {\n\treturn table.Load().(Table)\n}\n\n// SetTable sets the active routing table. A nil value\n// logs a warning and is ignored. The function is safe\n// to be called from multiple goroutines.\n// It also cleans up stale Prometheus metrics for targets that are no longer active.\nfunc SetTable(t Table) {\n\tif t == nil {\n\t\tlog.Print(\"[WARN] Ignoring nil routing table\")\n\t\treturn\n\t}\n\t// Get the old table to compare against\n\toldTable := GetTable()\n\t// Store the new table FIRST, then cleanup stale metrics.\n\t// This order is important to avoid a race condition where traffic\n\t// could recreate the metrics between delete and table swap.\n\ttable.Store(t)\n\t// Clean up metrics for removed targets after storing the new table\n\tcleanupStaleMetrics(oldTable, t)\n}\n\n// Table contains a set of routes grouped by host.\n// The host routes are sorted from most to least specific\n// by sorting the routes in reverse order by path.\ntype Table map[string]Routes\n\n// hostpath splits a 'host/path' prefix into 'host' and '/path' or it returns a\n// ':port' prefix as ':port' and ” since there is no path component for TCP\n// connections.\nfunc hostpath(prefix string) (host string, path string) {\n\tif strings.HasPrefix(prefix, \":\") {\n\t\treturn prefix, \"\"\n\t}\n\n\tp := strings.SplitN(prefix, \"/\", 2)\n\tif len(p) == 1 {\n\t\treturn p[0], \"/\"\n\t}\n\treturn p[0], \"/\" + p[1]\n}\n\nfunc NewTable(b *bytes.Buffer) (t Table, err error) {\n\tdefs, err := Parse(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tt = make(Table)\n\tfor _, d := range defs {\n\t\tswitch d.Cmd {\n\t\tcase RouteAddCmd:\n\t\t\terr = t.addRoute(d)\n\t\tcase RouteDelCmd:\n\t\t\terr = t.delRoute(d)\n\t\tcase RouteWeightCmd:\n\t\t\terr = t.weighRoute(d)\n\t\tdefault:\n\t\t\terr = fmt.Errorf(\"route: invalid command: %s\", d.Cmd)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Sort the route table for each hostname\n\tfor _, h := range t {\n\t\tsort.Sort(h)\n\t}\n\n\treturn t, nil\n}\n\nfunc NewTableCustom(defs *[]RouteDef) (t Table, err error) {\n\n\tt = make(Table)\n\tfor _, d := range *defs {\n\t\tswitch d.Cmd {\n\t\tcase RouteAddCmd:\n\t\t\terr = t.addRoute(&d)\n\t\tcase RouteDelCmd:\n\t\t\terr = t.delRoute(&d)\n\t\tcase RouteWeightCmd:\n\t\t\terr = t.weighRoute(&d)\n\t\tdefault:\n\t\t\terr = fmt.Errorf(\"route: invalid command: %s\", d.Cmd)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Sort the route table for each hostname\n\tfor _, h := range t {\n\t\tsort.Sort(h)\n\t}\n\n\treturn t, nil\n}\n\n// addRoute adds a new route prefix -> target for the given service.\nfunc (t Table) addRoute(d *RouteDef) error {\n\thost, path := hostpath(d.Src)\n\thost = strings.ToLower(host) // maintain compatibility with parseURLPrefixTag\n\n\tif d.Src == \"\" {\n\t\treturn errInvalidPrefix\n\t}\n\n\tif d.Dst == \"\" {\n\t\treturn errInvalidTarget\n\t}\n\n\ttargetURL, err := url.Parse(d.Dst)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route: invalid target. %s\", err)\n\t}\n\n\tswitch {\n\t// add new host\n\tcase t[host] == nil:\n\t\tg, err := glob.Compile(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr := &Route{Host: host, Path: path, Glob: g}\n\t\tr.addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts)\n\t\tt[host] = Routes{r}\n\n\t// add new route to existing host\n\tcase t[host].find(path) == nil:\n\t\tg, err := glob.Compile(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr := &Route{Host: host, Path: path, Glob: g}\n\t\tr.addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts)\n\t\tt[host] = append(t[host], r)\n\n\t// add new target to existing route\n\tdefault:\n\t\tt[host].find(path).addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts)\n\t}\n\n\treturn nil\n}\n\nfunc (t Table) weighRoute(d *RouteDef) error {\n\thost, path := hostpath(d.Src)\n\n\tif d.Src == \"\" {\n\t\treturn errInvalidPrefix\n\t}\n\n\tif t[host] == nil || t[host].find(path) == nil {\n\t\treturn errNoMatch\n\t}\n\n\tif n := t[host].find(path).setWeight(d.Service, d.Weight, d.Tags); n == 0 {\n\t\treturn errNoMatch\n\t}\n\treturn nil\n}\n\n// delRoute removes one or more routes depending on the arguments.\n// If service, prefix and target are provided then only this route\n// is removed. Are only service and prefix provided then all routes\n// for this service and prefix are removed. This removes all active\n// instances of the service from the route. If only the service is\n// provided then all routes for this service are removed. The service\n// will no longer receive traffic. Routes with no targets are removed.\nfunc (t Table) delRoute(d *RouteDef) error {\n\tswitch {\n\tcase len(d.Tags) > 0:\n\t\tfor _, routes := range t {\n\t\t\tfor _, r := range routes {\n\t\t\t\tr.filter(func(tg *Target) bool {\n\t\t\t\t\treturn (d.Service == \"\" || tg.Service == d.Service) && contains(tg.Tags, d.Tags)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\tcase d.Src == \"\" && d.Dst == \"\":\n\t\tfor _, routes := range t {\n\t\t\tfor _, r := range routes {\n\t\t\t\tr.filter(func(tg *Target) bool {\n\t\t\t\t\treturn tg.Service == d.Service\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\tcase d.Dst == \"\":\n\t\tr := t.route(hostpath(d.Src))\n\t\tif r == nil {\n\t\t\treturn nil\n\t\t}\n\t\tr.filter(func(tg *Target) bool {\n\t\t\treturn tg.Service == d.Service\n\t\t})\n\n\tdefault:\n\t\ttargetURL, err := url.Parse(d.Dst)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"route: invalid target. %s\", err)\n\t\t}\n\n\t\tr := t.route(hostpath(d.Src))\n\t\tif r == nil {\n\t\t\treturn nil\n\t\t}\n\t\tr.filter(func(tg *Target) bool {\n\t\t\treturn tg.Service == d.Service && tg.URL.String() == targetURL.String()\n\t\t})\n\t}\n\n\t// remove all routes without targets\n\tfor host, routes := range t {\n\t\tvar clone Routes\n\t\tfor _, r := range routes {\n\t\t\tif len(r.Targets) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tclone = append(clone, r)\n\t\t}\n\t\tt[host] = clone\n\t}\n\n\t// remove all hosts without routes\n\tfor host, routes := range t {\n\t\tif len(routes) == 0 {\n\t\t\tdelete(t, host)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// route finds the route for host/path or returns nil if none exists.\nfunc (t Table) route(host, path string) *Route {\n\troutes := t[host]\n\tif routes == nil {\n\t\treturn nil\n\t}\n\treturn routes.find(path)\n}\n\n// normalizeHost returns the hostname from the request\n// and removes the default port if present.\nfunc normalizeHost(host string, tls bool) string {\n\treturn strings.ToLower(normalizeHostNoLower(host, tls))\n}\n\nfunc normalizeHostNoLower(host string, tls bool) string {\n\tif !tls && strings.HasSuffix(host, \":80\") {\n\t\treturn host[:len(host)-len(\":80\")]\n\t}\n\tif tls && strings.HasSuffix(host, \":443\") {\n\t\treturn host[:len(host)-len(\":443\")]\n\t}\n\treturn host\n}\n\n// matchingHosts returns all keys (host name patterns) from the\n// routing table which match the normalized request hostname.\nfunc (t Table) matchingHosts(req *http.Request, globCache *GlobCache) (hosts []string) {\n\thost := normalizeHost(req.Host, req.TLS != nil)\n\tfor pattern := range t {\n\t\tnormpat := normalizeHost(pattern, req.TLS != nil)\n\n\t\t// Issue 548\n\t\t//\n\t\t//Get Compiled Glob from LRU cache\n\t\tg, err := globCache.Get(normpat)\n\t\tif err != nil {\n\t\t\tlog.Print(\"[Error] Compiling glob - \", err)\n\t\t\tg = glob.MustCompile(normpat)\n\t\t}\n\n\t\tif g.Match(host) {\n\t\t\thosts = append(hosts, pattern)\n\t\t}\n\t}\n\n\thosts = sortHostsReverseHostPort(hosts)\n\treturn\n}\n\n// Issue 548 - Added separate func\n//\n// matchingHostNoGlob returns the route from the\n// routing table which matches the normalized request hostname.\nfunc (t Table) matchingHostNoGlob(req *http.Request) (hosts []string) {\n\thost := normalizeHostNoLower(req.Host, req.TLS != nil)\n\n\tfor pattern := range t {\n\t\tnormpat := normalizeHost(pattern, req.TLS != nil)\n\t\tif normpat == host {\n\t\t\thosts = append(hosts, strings.ToLower(pattern))\n\t\t}\n\t}\n\thosts = sortHostsReverseHostPort(hosts)\n\treturn\n}\n\nfunc sortHostsReverseHostPort(hosts []string) []string {\n\t// Issue 506: multiple glob patterns hosts in wrong order\n\t//\n\t// DNS names have their most specific part at the front. In order to sort\n\t// them from most specific to least specific a lexicographic sort will\n\t// return the wrong result since it sorts by host name. *.foo.com will come\n\t// before *.a.foo.com even though the latter is more specific. To achieve\n\t// the correct result we need to reverse the strings, sort them and then\n\t// reverse them again.\n\tif len(hosts) < 2 {\n\t\treturn hosts\n\t}\n\tfor i, h := range hosts {\n\t\thosts[i] = ReverseHostPort(h)\n\t}\n\tsort.Sort(sort.Reverse(sort.StringSlice(hosts)))\n\tfor i, h := range hosts {\n\t\thosts[i] = ReverseHostPort(h)\n\t}\n\treturn hosts\n}\n\n// ReverseHostPort returns its argument string reversed rune-wise left to\n// right. If s includes a port, only the host part is reversed.\nfunc ReverseHostPort(s string) string {\n\th, p, _ := net.SplitHostPort(s)\n\tif h == \"\" {\n\t\th = s\n\t}\n\n\t// Taken from https://github.com/golang/example/blob/master/stringutil/reverse.go\n\tr := []rune(h)\n\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n\t\tr[i], r[j] = r[j], r[i]\n\t}\n\n\tif p == \"\" {\n\t\treturn string(r)\n\t} else {\n\t\treturn net.JoinHostPort(string(r), p)\n\t}\n}\n\n// Lookup finds a target url based on the current matcher and picker\n// or nil if there is none. It first checks the routes for the host\n// and if none matches then it falls back to generic routes without\n// a host. This is useful for a catch-all '/' rule.\nfunc (t Table) Lookup(req *http.Request, pick picker, match matcher, globCache *GlobCache, globDisabled bool) (target *Target) {\n\n\tvar hosts []string\n\n\t// find matching hosts for the request\n\t// and add \"no host\" as the fallback option\n\t// if globDisabled then match without Glob\n\t// Issue 548\n\tif globDisabled {\n\t\thosts = t.matchingHostNoGlob(req)\n\t} else {\n\t\thosts = t.matchingHosts(req, globCache)\n\t}\n\n\thosts = append(hosts, \"\")\n\tfor _, h := range hosts {\n\t\tif target = t.lookup(h, req.URL.Path, pick, match); target != nil {\n\t\t\tif target.RedirectCode != 0 {\n\t\t\t\treq.URL.Host = req.Host\n\t\t\t\ttarget.BuildRedirectURL(req.URL) // build redirect url and cache in target\n\t\t\t\tif target.RedirectURL.Scheme == req.Header.Get(\"X-Forwarded-Proto\") &&\n\t\t\t\t\ttarget.RedirectURL.Host == req.Host &&\n\t\t\t\t\ttarget.RedirectURL.Path == req.URL.Path {\n\t\t\t\t\tlog.Print(\"[INFO] Skipping redirect with same scheme, host and path\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn target\n}\n\nfunc (t Table) LookupHost(host string, pick picker) *Target {\n\treturn t.lookup(host, \"/\", pick, prefixMatcher)\n}\n\nfunc (t Table) lookup(host, path string, pick picker, match matcher) *Target {\n\thost = strings.ToLower(host) // routes are always added lowercase\n\tfor _, r := range t[host] {\n\t\tif match(path, r) {\n\t\t\tn := len(r.Targets)\n\t\t\tif n == 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tvar target *Target\n\t\t\tif n == 1 {\n\t\t\t\ttarget = r.Targets[0]\n\t\t\t} else {\n\t\t\t\ttarget = pick(r)\n\t\t\t}\n\t\t\treturn target\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (t Table) config(addWeight bool) []string {\n\tvar hosts []string\n\tfor host := range t {\n\t\tif host != \"\" {\n\t\t\thosts = append(hosts, host)\n\t\t}\n\t}\n\tsort.Sort(sort.Reverse(sort.StringSlice(hosts)))\n\n\t// entries without host come last\n\thosts = append(hosts, \"\")\n\n\tvar cfg []string\n\tfor _, host := range hosts {\n\t\tfor _, routes := range t[host] {\n\t\t\tcfg = append(cfg, routes.config(addWeight)...)\n\t\t}\n\t}\n\treturn cfg\n}\n\n// String returns the routing table as config file which can\n// be read by Parse() again.\nfunc (t Table) String() string {\n\treturn strings.Join(t.config(false), \"\\n\")\n}\n\n// Dump returns the routing table as a detailed\nfunc (t Table) Dump() string {\n\tw := new(bytes.Buffer)\n\n\thosts := []string{}\n\tfor k := range t {\n\t\thosts = append(hosts, k)\n\t}\n\tsort.Strings(hosts)\n\n\tlast := func(n, total int) bool {\n\t\treturn n == total-1\n\t}\n\n\tfor i, h := range hosts {\n\t\tfmt.Fprintf(w, \"+-- host=%s\\n\", h)\n\n\t\troutes := t[h]\n\t\tfor j, r := range routes {\n\t\t\tp0 := \"|   \"\n\t\t\tif last(i, len(hosts)) {\n\t\t\t\tp0 = \"    \"\n\t\t\t}\n\t\t\tp1 := \"|-- \"\n\t\t\tif last(j, len(routes)) {\n\t\t\t\tp1 = \"+-- \"\n\t\t\t}\n\n\t\t\tfmt.Fprintf(w, \"%s%spath=%s\\n\", p0, p1, r.Path)\n\n\t\t\tm := map[*Target]int{}\n\t\t\tfor _, t := range r.wTargets {\n\t\t\t\tm[t] += 1\n\t\t\t}\n\n\t\t\ttotal := len(r.wTargets)\n\t\t\tk := 0\n\t\t\tfor t, n := range m {\n\t\t\t\tp1 := \"|    \"\n\t\t\t\tif last(j, len(routes)) {\n\t\t\t\t\tp1 = \"    \"\n\t\t\t\t}\n\t\t\t\tp2 := \"|-- \"\n\t\t\t\tif last(k, len(m)) {\n\t\t\t\t\tp2 = \"+-- \"\n\t\t\t\t}\n\t\t\t\tweight := float64(n) / float64(total)\n\t\t\t\tfmt.Fprintf(w, \"%s%s%saddr=%s weight %2.2f slots %d/%d\\n\", p0, p1, p2, t.URL.Host, weight, n, total)\n\t\t\t\tk++\n\t\t\t}\n\t\t}\n\t}\n\treturn w.String()\n}\n"
  },
  {
    "path": "route/table_registry_test.go",
    "content": "package route\n\n// func TestSyncRegistry(t *testing.T) {\n// \toldRegistry := ServiceRegistry\n// \tServiceRegistry = newStubRegistry()\n// \tdefer func() { ServiceRegistry = oldRegistry }()\n//\n// \ttbl := make(Table)\n// \ttbl.addRoute(&RouteDef{Service: \"svc-a\", Src: \"/aaa\", Dst: \"http://localhost:1234\", Weight: 1})\n// \ttbl.addRoute(&RouteDef{Service: \"svc-b\", Src: \"/bbb\", Dst: \"http://localhost:5678\", Weight: 1})\n// \tif got, want := ServiceRegistry.Names(), []string{\"svc-a._./aaa.localhost_1234\", \"svc-b._./bbb.localhost_5678\"}; !reflect.DeepEqual(got, want) {\n// \t\tt.Fatalf(\"got %v want %v\", got, want)\n// \t}\n//\n// \ttbl.delRoute(&RouteDef{Service: \"svc-b\", Src: \"/bbb\", Dst: \"http://localhost:5678\"})\n// \tsyncRegistry(tbl)\n// \tif got, want := ServiceRegistry.Names(), []string{\"svc-a._./aaa.localhost_1234\"}; !reflect.DeepEqual(got, want) {\n// \t\tt.Fatalf(\"got %v want %v\", got, want)\n// \t}\n// }\n//\n// func newStubRegistry() metrics.Registry {\n// \treturn &stubRegistry{names: make(map[string]bool)}\n// }\n//\n// type stubRegistry struct {\n// \tnames map[string]bool\n// }\n//\n// func (p *stubRegistry) Names() []string {\n// \tn := []string{}\n// \tfor k := range p.names {\n// \t\tn = append(n, k)\n// \t}\n// \tsort.Strings(n)\n// \treturn n\n// }\n//\n// func (p *stubRegistry) Unregister(name string) {\n// \tdelete(p.names, name)\n// }\n//\n// func (p *stubRegistry) UnregisterAll() {\n// \tp.names = map[string]bool{}\n// }\n//\n// func (p *stubRegistry) GetCounter(name string) metrics.Counter {\n// \tp.names[name] = true\n// \treturn metrics.NoopCounter{}\n// }\n//\n// func (p *stubRegistry) GetTimer(name string) metrics.Timer {\n// \tp.names[name] = true\n// \treturn metrics.NoopTimer{}\n// }\n"
  },
  {
    "path": "route/table_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n)\n\nconst (\n\t// helper constants for the Lookup function\n\tglobEnabled  = false\n\tglobDisabled = true\n)\n\n// Global GlobCache for Testing\nvar globCache = NewGlobCache(1000)\n\nfunc TestTableParse(t *testing.T) {\n\tgenRoutes := func(n int, format string) (a []string) {\n\t\tfor i := range n {\n\t\t\ta = append(a, fmt.Sprintf(format, i))\n\t\t}\n\t\treturn a\n\t}\n\n\ttests := []struct {\n\t\tdesc    string\n\t\tin, out []string\n\t}{\n\n\t\t{\"1 service, 1 prefix\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"1 service, 1 prefix, 3 instances\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com:1111/`,\n\t\t\t\t`route add svc-a / http://aaa.com:2222/`,\n\t\t\t\t`route add svc-a / http://aaa.com:3333/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com:1111/ weight 0.3333`,\n\t\t\t\t`route add svc-a / http://aaa.com:2222/ weight 0.3333`,\n\t\t\t\t`route add svc-a / http://aaa.com:3333/ weight 0.3333`,\n\t\t\t},\n\t\t},\n\n\t\t{\"1 service, 1 prefix with option\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ opts \"strip=/foo\"`,\n\t\t\t\t`route add svc-b / http://bbb.com/ opts \"strip=/bar\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 0.5000 opts \"strip=/foo\"`,\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 0.5000 opts \"strip=/bar\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"1 service, 1 prefix, 2 instances with different options\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ opts \"strip=/foo\"`,\n\t\t\t\t`route add svc-b / http://bbb.com/ opts \"strip=/bar\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 0.5000 opts \"strip=/foo\"`,\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 0.5000 opts \"strip=/bar\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"2 service, 1 prefix\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-b / http://bbb.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 0.5000`,\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 0.5000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"1 service, 2 prefix\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /one http://aaa.com/`,\n\t\t\t\t`route add svc-a /two http://aaa.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /two http://aaa.com/ weight 1.0000`,\n\t\t\t\t`route add svc-a /one http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"2 service, 2 prefix\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /a http://aaa.com/`,\n\t\t\t\t`route add svc-b /b http://bbb.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b /b http://bbb.com/ weight 1.0000`,\n\t\t\t\t`route add svc-a /a http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"sort by more specific prefix\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-b /b http://bbb.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b /b http://bbb.com/ weight 1.0000`,\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"sort prefix with host before prefix without host\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-b b.com/ http://bbb.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b b.com/ http://bbb.com/ weight 1.0000`,\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"add more specific prefix to existing host\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a a.com/ http://aaa.com/`,\n\t\t\t\t`route add svc-a a.com/a http://aaa.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a a.com/a http://aaa.com/ weight 1.0000`,\n\t\t\t\t`route add svc-a a.com/ http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"delete route by service, path and target\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-b / http://bbb.com/`,\n\t\t\t\t`route del svc-b / http://bbb.com/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"delete route by service and path\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-a / http://aaa.com:2222/`,\n\t\t\t\t`route add svc-b / http://bbb.com/`,\n\t\t\t\t`route del svc-a /`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"delete route by service\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /a http://aaa.com/`,\n\t\t\t\t`route add svc-a / http://aaa.com/`,\n\t\t\t\t`route add svc-b / http://bbb.com/`,\n\t\t\t\t`route del svc-a`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"delete route by service and tags\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /a http://aaa.com/ tags \"a,b\"`,\n\t\t\t\t`route add svc-a /  http://aaa.com/ tags \"b,c\"`,\n\t\t\t\t`route add svc-b /  http://bbb.com/ tags \"c,d\"`,\n\t\t\t\t`route del svc-a tags \"a,b\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-a / http://aaa.com/ weight 0.5000 tags \"b,c\"`,\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 0.5000 tags \"c,d\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"delete route by tags\",\n\t\t\t[]string{\n\t\t\t\t`route add svc-a /a http://aaa.com/ tags \"a,b\"`,\n\t\t\t\t`route add svc-a /  http://aaa.com/ tags \"b,c\"`,\n\t\t\t\t`route add svc-b /  http://bbb.com/ tags \"c,d\"`,\n\t\t\t\t`route del tags \"b\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc-b / http://bbb.com/ weight 1.0000 tags \"c,d\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh fixed weight 0 -> auto distribution\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh only fixed weights and sum(fixedWeight) < 1 -> normalize to 100%\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.2`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.3`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.4000`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.6000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh only fixed weights and sum(fixedWeight) > 1 -> normalize to 100%\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 2`,\n\t\t\t\t`route add svc / http://bar:222/ weight 3`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.4000`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.6000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh multiple entries for same instance with no fixed weight -> de-duplication\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 1.0000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh multiple entries with no fixed weight -> even distribution\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:222/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.5000`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.5000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh multiple entries with de-dup and no fixed weight -> even distribution\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:222/`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.5000`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.5000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh mixed fixed and auto weights -> even distribution of remaining weight across non-fixed weighted targets\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:222/`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.5`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.2500`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.2500`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.5000`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh fixed weight == 100% -> route only to fixed weighted targets\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.2500`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.7500`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:222/ weight 0.2500`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.7500`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh fixed weight > 100%  -> route only to fixed weighted targets and normalize weight\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/`,\n\t\t\t\t`route add svc / http://bar:222/ weight 1`,\n\t\t\t\t`route add svc / http://bar:333/ weight 3`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:222/ weight 0.2500`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.7500`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh dynamic weight matched on service name\",\n\t\t\t[]string{\n\t\t\t\t`route add svca / http://bar:111/`,\n\t\t\t\t`route add svcb / http://bar:222/`,\n\t\t\t\t`route add svcb / http://bar:333/`,\n\t\t\t\t`route weight svcb / weight 0.1`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svca / http://bar:111/ weight 0.9000`,\n\t\t\t\t`route add svcb / http://bar:222/ weight 0.0500`,\n\t\t\t\t`route add svcb / http://bar:333/ weight 0.0500`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh dynamic weight matched on service name and tags\",\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ tags \"a\"`,\n\t\t\t\t`route add svc / http://bar:222/ tags \"b\"`,\n\t\t\t\t`route add svc / http://bar:333/ tags \"b\"`,\n\t\t\t\t`route weight svc / weight 0.1 tags \"b\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svc / http://bar:111/ weight 0.9000 tags \"a\"`,\n\t\t\t\t`route add svc / http://bar:222/ weight 0.0500 tags \"b\"`,\n\t\t\t\t`route add svc / http://bar:333/ weight 0.0500 tags \"b\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh dynamic weight matched on tags\",\n\t\t\t[]string{\n\t\t\t\t`route add svca / http://bar:111/ tags \"a\"`,\n\t\t\t\t`route add svcb / http://bar:222/ tags \"b\"`,\n\t\t\t\t`route add svcb / http://bar:333/ tags \"b\"`,\n\t\t\t\t`route weight / weight 0.1 tags \"b\"`,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t`route add svca / http://bar:111/ weight 0.9000 tags \"a\"`,\n\t\t\t\t`route add svcb / http://bar:222/ weight 0.0500 tags \"b\"`,\n\t\t\t\t`route add svcb / http://bar:333/ weight 0.0500 tags \"b\"`,\n\t\t\t},\n\t\t},\n\n\t\t{\"weigh more than 1000 routes\",\n\t\t\tgenRoutes(1234, `route add svc / http://bar:%d/`),\n\t\t\tgenRoutes(1234, `route add svc / http://bar:%d/ weight 0.0008`),\n\t\t},\n\n\t\t{\"weigh more than 1000 routes with a fixed route target\",\n\t\t\tfunc() (a []string) {\n\t\t\t\ta = genRoutes(1234, `route add svc / http://bar:%d/`)\n\t\t\t\ta = append(a, `route add svc / http://static:12345/ tags \"a\"`)\n\t\t\t\ta = append(a, `route weight svc / weight 0.2 tags \"a\"`)\n\t\t\t\treturn a\n\t\t\t}(),\n\t\t\tfunc() (a []string) {\n\t\t\t\ta = genRoutes(1234, `route add svc / http://bar:%d/ weight 0.0006`)\n\t\t\t\ta = append(a, `route add svc / http://static:12345/ weight 0.2000 tags \"a\"`)\n\t\t\t\treturn a\n\t\t\t}(),\n\t\t},\n\t}\n\n\tatof := func(s string) float64 {\n\t\tn, err := strconv.ParseFloat(s, 64)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn n\n\t}\n\n\tfor _, tt := range tests {\n\t\t// perform a test which parses the tt.in routes into a table and\n\t\t// compares the weighted, generated routing table with tt.out. verify,\n\t\t// that the distribution of the target URLs for each prefix in the\n\t\t// generated routing table matches the weight This test assumes that\n\t\t// the table generates the correct routing table but does not test the\n\t\t// actual lookup which it probably should.\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\t// parse the routes\n\t\t\ttbl, err := NewTable(bytes.NewBufferString(strings.Join(tt.in, \"\\n\")))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"got %v want nil\", err)\n\t\t\t}\n\n\t\t\t// compare the generated routes with the normalized weights\n\t\t\tif got, want := tbl.config(true), tt.out; !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"got\\n%s\\nwant\\n%s\", strings.Join(got, \"\\n\"), strings.Join(want, \"\\n\"))\n\t\t\t}\n\n\t\t\t// check that the weights returned in the generated config match\n\t\t\t// the distribution in the wTargets array of the corresponding route.\n\t\t\tchecked := map[string]bool{}\n\n\t\t\tfor _, s := range tt.out {\n\t\t\t\t// route add <svc> <path> ...\n\t\t\t\tpath := strings.Fields(s)[3]\n\n\t\t\t\t// if we have already checked this path then skip this.\n\t\t\t\t// Otherwise, this test becomes O(n^2) and will time out for\n\t\t\t\t// the large number of routes.\n\t\t\t\tif checked[path] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tchecked[path] = true\n\n\t\t\t\t// fetch the route\n\t\t\t\tr := tbl.route(hostpath(path))\n\t\t\t\tif r == nil {\n\t\t\t\t\tt.Fatalf(\"got nil want route %s\", path)\n\t\t\t\t}\n\n\t\t\t\t// check that there are at least some slots\n\t\t\t\tif len(r.wTargets) == 0 {\n\t\t\t\t\tt.Fatalf(\"got 0 targets want some\")\n\t\t\t\t}\n\n\t\t\t\t// pre-generate the target urls for comparison as this\n\t\t\t\t// will otherwise slow the test down significantly\n\t\t\t\ttargetURLs := make([]string, len(r.wTargets))\n\t\t\t\tfor i, tg := range r.wTargets {\n\t\t\t\t\ttargetURLs[i] = tg.URL.Scheme + \"://\" + tg.URL.Host + tg.URL.Path\n\t\t\t\t}\n\n\t\t\t\t// count how often the 'url' from 'route add svc <path> <url>'\n\t\t\t\t// appears in the list of wTargets for all the URLs\n\t\t\t\t// from the routes to determine whether the actual\n\t\t\t\t// distribution of each target within the wTarget slice\n\t\t\t\t// matches what we expect\n\t\t\t\tfor _, s := range tt.out {\n\t\t\t\t\t// route add <svc> <path> <url> weight <weight> ...`,\n\t\t\t\t\tp := strings.Fields(s)\n\n\t\t\t\t\t// skip if the path doesn't match\n\t\t\t\t\tif path != p[3] {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// count how often the target url appears in the list of wTargets\n\t\t\t\t\tcount := 0\n\t\t\t\t\tfor _, u := range targetURLs {\n\t\t\t\t\t\tif u == p[4] {\n\t\t\t\t\t\t\tcount++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// calc the weight as nSlots/totalSlots\n\t\t\t\t\tgotWeight := float64(count) / float64(len(r.wTargets))\n\n\t\t\t\t\t// round the weight down to the number of decimal points\n\t\t\t\t\t// supported by maxSlots\n\t\t\t\t\tgotWeight = float64(int(gotWeight*float64(maxSlots))) / float64(maxSlots)\n\n\t\t\t\t\t// compare to the weight from the generated config\n\t\t\t\t\twantWeight := atof(p[6])\n\n\t\t\t\t\t// check that the actual weight is within 2% of the computed weight\n\t\t\t\t\tif math.Abs(gotWeight-wantWeight) > 0.02 {\n\t\t\t\t\t\tt.Errorf(\"got weight %f want %f\", gotWeight, wantWeight)\n\t\t\t\t\t}\n\n\t\t\t\t\t// TODO(fs): verify distriibution of targets across the ring\n\t\t\t\t\t// TODO(fs): verify lookup with 'rr' works as expected. Current test is by proxy of generated config.\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNormalizeHost(t *testing.T) {\n\ttests := []struct {\n\t\treq  *http.Request\n\t\thost string\n\t}{\n\t\t{&http.Request{Host: \"foo.com\"}, \"foo.com\"},\n\t\t{&http.Request{Host: \"foo.com:80\"}, \"foo.com\"},\n\t\t{&http.Request{Host: \"foo.com:81\"}, \"foo.com:81\"},\n\t\t{&http.Request{Host: \"foo.com\", TLS: &tls.ConnectionState{}}, \"foo.com\"},\n\t\t{&http.Request{Host: \"foo.com:443\", TLS: &tls.ConnectionState{}}, \"foo.com\"},\n\t\t{&http.Request{Host: \"foo.com:444\", TLS: &tls.ConnectionState{}}, \"foo.com:444\"},\n\t}\n\n\tfor i, tt := range tests {\n\t\tif got, want := normalizeHost(tt.req.Host, tt.req.TLS != nil), tt.host; got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\n// see https://github.com/fabiolb/fabio/issues/448\n// for more information on the issue and purpose of this test\nfunc TestTableLookupIssue448(t *testing.T) {\n\ts := `\n\troute add mock foo.com:80/ https://foo.com/ opts \"redirect=301\"\n\troute add mock aaa.com:80/ http://bbb.com/ opts \"redirect=301\"\n\troute add mock ccc.com:443/bar https://ccc.com/baz opts \"redirect=301\"\n\troute add mock / http://foo.com/\n\t`\n\n\ttbl, err := NewTable(bytes.NewBufferString(s))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar tests = []struct {\n\t\treq         *http.Request\n\t\tdst         string\n\t\tglobEnabled bool\n\t}{\n\t\t{\n\t\t\treq: &http.Request{\n\t\t\t\tHost: \"foo.com\",\n\t\t\t\tURL:  mustParse(\"/\"),\n\t\t\t},\n\t\t\tdst: \"https://foo.com/\",\n\t\t\t// empty upstream header should follow redirect - standard behavior\n\t\t},\n\t\t{\n\t\t\treq: &http.Request{\n\t\t\t\tHost:   \"foo.com\",\n\t\t\t\tURL:    mustParse(\"/\"),\n\t\t\t\tHeader: http.Header{\"X-Forwarded-Proto\": {\"http\"}},\n\t\t\t},\n\t\t\tdst: \"https://foo.com/\",\n\t\t\t// upstream http request to same host and path should follow redirect\n\t\t},\n\t\t{\n\t\t\treq: &http.Request{\n\t\t\t\tHost:   \"foo.com\",\n\t\t\t\tURL:    mustParse(\"/\"),\n\t\t\t\tHeader: http.Header{\"X-Forwarded-Proto\": {\"https\"}},\n\t\t\t\tTLS:    &tls.ConnectionState{},\n\t\t\t},\n\t\t\tdst: \"http://foo.com/\",\n\t\t\t// upstream https request to same host and path should NOT follow redirect\"\n\t\t},\n\t\t{\n\t\t\treq: &http.Request{\n\t\t\t\tHost:   \"aaa.com\",\n\t\t\t\tURL:    mustParse(\"/\"),\n\t\t\t\tHeader: http.Header{\"X-Forwarded-Proto\": {\"http\"}},\n\t\t\t},\n\t\t\tdst: \"http://bbb.com/\",\n\t\t\t// upstream http request to different http host should follow redirect\n\t\t},\n\t\t{\n\t\t\treq: &http.Request{\n\t\t\t\tHost:   \"ccc.com\",\n\t\t\t\tURL:    mustParse(\"/bar\"),\n\t\t\t\tHeader: http.Header{\"X-Forwarded-Proto\": {\"https\"}},\n\t\t\t\tTLS:    &tls.ConnectionState{},\n\t\t\t},\n\t\t\tdst: \"https://ccc.com/baz\",\n\t\t\t// upstream https request to same https host with different path should follow redirect\"\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\tif got, want := tbl.Lookup(tt.req, rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestTableLookup(t *testing.T) {\n\ts := `\n\troute add svc / http://foo.com:800\n\troute add svc /foo http://foo.com:900\n\troute add svc abc.com/ http://foo.com:1000\n\troute add svc abc.com/foo http://foo.com:1500\n\troute add svc abc.com/foo/ http://foo.com:2000\n\troute add svc abc.com/foo/bar http://foo.com:2500\n\troute add svc abc.com/foo/bar/ http://foo.com:3000\n\troute add svc z.abc.com/foo/ http://foo.com:3100\n\troute add svc *.abc.com/ http://foo.com:4000\n\troute add svc *.abc.com/foo/ http://foo.com:5000\n\troute add svc *.aaa.abc.com/ http://foo.com:6000\n\troute add svc *.bbb.abc.com/ http://foo.com:6100\n\troute add svc xyz.com:80/ https://xyz.com\n\t`\n\n\ttbl, err := NewTable(bytes.NewBufferString(s))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar tests = []struct {\n\t\treq         *http.Request\n\t\tdst         string\n\t\tglobEnabled bool\n\t}{\n\t\t// match on host and path with and without trailing slash\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/\")}, \"http://foo.com:1000\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/bar\")}, \"http://foo.com:1000\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/foo\")}, \"http://foo.com:1500\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:2000\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/foo/bar\")}, \"http://foo.com:2500\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com\", URL: mustParse(\"/foo/bar/\")}, \"http://foo.com:3000\", globEnabled},\n\n\t\t// do not match on host but maybe on path\n\t\t{&http.Request{Host: \"def.com\", URL: mustParse(\"/\")}, \"http://foo.com:800\", globEnabled},\n\t\t{&http.Request{Host: \"def.com\", URL: mustParse(\"/bar\")}, \"http://foo.com:800\", globEnabled},\n\t\t{&http.Request{Host: \"def.com\", URL: mustParse(\"/foo\")}, \"http://foo.com:900\", globEnabled},\n\n\t\t// strip default port\n\t\t{&http.Request{Host: \"abc.com:80\", URL: mustParse(\"/\")}, \"http://foo.com:1000\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com:443\", URL: mustParse(\"/\"), TLS: &tls.ConnectionState{}}, \"http://foo.com:1000\", globEnabled},\n\n\t\t// not using default port\n\t\t{&http.Request{Host: \"abc.com:443\", URL: mustParse(\"/\")}, \"http://foo.com:800\", globEnabled},\n\t\t{&http.Request{Host: \"abc.com:80\", URL: mustParse(\"/\"), TLS: &tls.ConnectionState{}}, \"http://foo.com:800\", globEnabled},\n\n\t\t// glob match the host\n\t\t{&http.Request{Host: \"x.abc.com\", URL: mustParse(\"/\")}, \"http://foo.com:4000\", globEnabled},\n\t\t{&http.Request{Host: \"y.abc.com\", URL: mustParse(\"/abc\")}, \"http://foo.com:4000\", globEnabled},\n\t\t{&http.Request{Host: \"x.abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:5000\", globEnabled},\n\t\t{&http.Request{Host: \"y.abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:5000\", globEnabled},\n\t\t{&http.Request{Host: \".abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:5000\", globEnabled},\n\t\t{&http.Request{Host: \"x.y.abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:5000\", globEnabled},\n\t\t{&http.Request{Host: \"y.abc.com:80\", URL: mustParse(\"/foo/\")}, \"http://foo.com:5000\", globEnabled},\n\t\t{&http.Request{Host: \"x.aaa.abc.com\", URL: mustParse(\"/\")}, \"http://foo.com:6000\", globEnabled},\n\t\t{&http.Request{Host: \"x.aaa.abc.com\", URL: mustParse(\"/foo\")}, \"http://foo.com:6000\", globEnabled},\n\t\t{&http.Request{Host: \"x.bbb.abc.com\", URL: mustParse(\"/\")}, \"http://foo.com:6100\", globEnabled},\n\t\t{&http.Request{Host: \"x.bbb.abc.com\", URL: mustParse(\"/foo\")}, \"http://foo.com:6100\", globEnabled},\n\t\t{&http.Request{Host: \"y.abc.com:443\", URL: mustParse(\"/foo/\"), TLS: &tls.ConnectionState{}}, \"http://foo.com:5000\", globEnabled},\n\n\t\t// exact match has precedence over glob match\n\t\t{&http.Request{Host: \"z.abc.com\", URL: mustParse(\"/foo/\")}, \"http://foo.com:3100\", globEnabled},\n\n\t\t// explicit port on route\n\t\t{&http.Request{Host: \"xyz.com\", URL: mustParse(\"/\")}, \"https://xyz.com\", globEnabled},\n\t}\n\n\tfor i, tt := range tests {\n\t\tif got, want := tbl.Lookup(tt.req, rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want {\n\t\t\tt.Errorf(\"%d: got %v want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestTableLookup_656(t *testing.T) {\n\t// A typical HTTPS redirect\n\ts := `\n\troute add my-service example.com:80/ https://example.com$path opts \"redirect=301\"\n\troute add my-service example.com/ http://127.0.0.1:3000/\n\t`\n\n\ttbl, err := NewTable(bytes.NewBufferString(s))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/foo\", nil)\n\ttarget := tbl.Lookup(req, rrPicker, prefixMatcher, globCache, globDisabled)\n\n\tif target == nil {\n\t\tt.Fatal(\"No route match\")\n\t}\n\tif got, want := target.RedirectCode, 301; got != want {\n\t\tt.Errorf(\"target.RedirectCode = %d, want %d\", got, want)\n\t}\n\tif got, want := fmt.Sprint(target.RedirectURL), \"https://example.com/foo\"; got != want {\n\t\tt.Errorf(\"target.RedirectURL = %s, want %s\", got, want)\n\t}\n}\n\nfunc TestNewTableCustom(t *testing.T) {\n\n\tvar routes []RouteDef\n\tvar tags = []string{\"tag1\", \"tag2\"}\n\tvar opts = make(map[string]string)\n\topts[\"tlsskipverify\"] = \"true\"\n\topts[\"proto\"] = \"http\"\n\n\tvar route1 = RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service1\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.1:8080\",\n\t\tWeight:  0.50,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\tvar route2 = RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service1\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.2:8080\",\n\t\tWeight:  0.50,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\tvar route3 = RouteDef{\n\t\tCmd:     \"route add\",\n\t\tService: \"service2\",\n\t\tSrc:     \"app.com\",\n\t\tDst:     \"http://10.1.1.3:8080\",\n\t\tWeight:  0.25,\n\t\tTags:    tags,\n\t\tOpts:    opts,\n\t}\n\n\troutes = append(routes, route1)\n\troutes = append(routes, route2)\n\troutes = append(routes, route3)\n\n\ttable, err := NewTableCustom(&routes)\n\n\tif err != nil {\n\t\tfmt.Printf(\"Got error from NewTableCustom - %s\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\ttableString := table.String()\n\tif !strings.Contains(tableString, route1.Dst) {\n\t\tfmt.Printf(\"Table Missing Destination %s -- Table -- %s\", route1.Dst, tableString)\n\t\tt.FailNow()\n\t}\n\n\tif !strings.Contains(tableString, route2.Dst) {\n\t\tfmt.Printf(\"Table Missing Destination %s -- Table -- %s\", route1.Dst, tableString)\n\t\tt.FailNow()\n\t}\n\n\tif !strings.Contains(tableString, route3.Dst) {\n\t\tfmt.Printf(\"Table Missing Destination %s -- Table -- %s\", route1.Dst, tableString)\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestTable_Dump(t *testing.T) {\n\ts := `\n\troute add svc / http://foo.com:800\n\troute add svc /foo http://foo.com:900\n\troute add svc abc.com/ http://foo.com:1000\n\t`\n\n\ttbl, err := NewTable(bytes.NewBufferString(s))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twant := `+-- host=\n|   |-- path=/foo\n|   |    +-- addr=foo.com:900 weight 1.00 slots 1/1\n|   +-- path=/\n|       +-- addr=foo.com:800 weight 1.00 slots 1/1\n+-- host=abc.com\n    +-- path=/\n        +-- addr=foo.com:1000 weight 1.00 slots 1/1\n`\n\n\tgot := tbl.Dump()\n\n\tif want != got {\n\t\tt.Errorf(\"Unexpected Dump() output:\\nwant:\\n%s\\ngot:\\n%s\\n\", want, got)\n\t}\n}\n"
  },
  {
    "path": "route/target.go",
    "content": "package route\n\nimport (\n\tgkm \"github.com/go-kit/kit/metrics\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype Target struct {\n\n\t// Histogram measures throughput and latency of this target\n\tTimer gkm.Histogram\n\n\t// Counters for rx and tx\n\tRxCounter gkm.Counter\n\tTxCounter gkm.Counter\n\n\t// Opts is the raw options for the target.\n\tOpts map[string]string\n\n\t// URL is the endpoint the service instance listens on\n\tURL *url.URL\n\n\t// RedirectURL is the redirect target based on the request.\n\t// This is cached here to prevent multiple generations per request.\n\tRedirectURL *url.URL\n\n\t// accessRules is map of access information for the target.\n\taccessRules map[string][]interface{}\n\n\t// Transport allows for different types of transports\n\tTransport *http.Transport\n\t// Service is the name of the service the targetURL points to\n\tService string\n\n\t// StripPath will be removed from the front of the outgoing\n\t// request path\n\tStripPath string\n\n\t// PrependPath will be added to the front of the outgoing\n\t// request path (after StripPath has been removed)\n\tPrependPath string\n\n\t// Host signifies what the proxy will set the Host header to.\n\t// The proxy does not modify the Host header by default.\n\t// When Host is set to 'dst' the proxy will use the host name\n\t// of the target host for the outgoing request.\n\tHost string\n\n\t// name of the auth handler for this target\n\tAuthScheme string\n\n\t// Tags are the list of tags for this target\n\tTags []string\n\n\t// RedirectCode is the HTTP status code used for redirects.\n\t// When set to a value > 0 the client is redirected to the target url.\n\tRedirectCode int\n\n\t// FixedWeight is the weight assigned to this target.\n\t// If the value is 0 the targets weight is dynamic.\n\tFixedWeight float64\n\n\t// Weight is the actual weight for this service in percent.\n\tWeight float64\n\n\t// TLSSkipVerify disables certificate validation for upstream\n\t// TLS connections.\n\tTLSSkipVerify bool\n\n\t// ProxyProto enables PROXY Protocol on upstream connection\n\tProxyProto bool\n}\n\nfunc (t *Target) BuildRedirectURL(requestURL *url.URL) {\n\tt.RedirectURL = &url.URL{\n\t\tScheme:   t.URL.Scheme,\n\t\tHost:     t.URL.Host,\n\t\tPath:     t.URL.Path,\n\t\tRawPath:  t.URL.Path,\n\t\tRawQuery: t.URL.RawQuery,\n\t}\n\t// treat case of $path not separated with a / from host\n\tif strings.HasSuffix(t.RedirectURL.Host, \"$path\") {\n\t\tt.RedirectURL.Host = t.RedirectURL.Host[:len(t.RedirectURL.Host)-len(\"$path\")]\n\t\tt.RedirectURL.Path = \"$path\"\n\t}\n\t// remove / before $path in redirect url\n\tif strings.Contains(t.RedirectURL.Path, \"/$path\") {\n\t\tt.RedirectURL.Path = strings.Replace(t.RedirectURL.Path, \"/$path\", \"$path\", 1)\n\t\tt.RedirectURL.RawPath = strings.Replace(t.RedirectURL.RawPath, \"/$path\", \"$path\", 1)\n\t}\n\t// remove strip path, insert passed request path, set query\n\tif strings.Contains(t.RedirectURL.Path, \"$path\") {\n\t\t// set replacement paths\n\t\treplacePath := requestURL.Path\n\t\tvar replaceRawPath string\n\t\tif requestURL.RawPath == \"\" {\n\t\t\treplaceRawPath = requestURL.Path\n\t\t} else {\n\t\t\treplaceRawPath = requestURL.RawPath\n\t\t}\n\t\t// strip path before replacement\n\t\tif t.StripPath != \"\" {\n\t\t\treplacePath = strings.TrimPrefix(replacePath, t.StripPath)\n\t\t\treplaceRawPath = strings.TrimPrefix(replaceRawPath, t.StripPath)\n\t\t}\n\t\t// add prepend path\n\t\tif t.PrependPath != \"\" {\n\t\t\treplacePath = t.PrependPath + replacePath\n\t\t\treplaceRawPath = t.PrependPath + replaceRawPath\n\t\t}\n\t\t// do path replacement\n\t\tt.RedirectURL.Path = strings.Replace(t.RedirectURL.Path, \"$path\", replacePath, 1)\n\t\tt.RedirectURL.RawPath = strings.Replace(t.RedirectURL.RawPath, \"$path\", replaceRawPath, 1)\n\t\t// set query\n\t\tif t.RedirectURL.RawQuery == \"\" && requestURL.RawQuery != \"\" {\n\t\t\tt.RedirectURL.RawQuery = requestURL.RawQuery\n\t\t}\n\t}\n\tif t.RedirectURL.Path == \"\" {\n\t\tt.RedirectURL.Path = \"/\"\n\t}\n\tt.RedirectURL.Host = strings.Replace(t.RedirectURL.Host, \"$host\", requestURL.Host, 1)\n}\n"
  },
  {
    "path": "route/target_test.go",
    "content": "package route\n\nimport (\n\t\"bytes\"\n\t\"net/url\"\n\t\"testing\"\n)\n\nfunc TestTarget_BuildRedirectURL(t *testing.T) {\n\ttype routeTest struct {\n\t\treq  string\n\t\twant string\n\t}\n\ttests := []struct {\n\t\troute string\n\t\ttests []routeTest\n\t}{\n\t\t{ // simple absolute redirect\n\t\t\troute: \"route add svc / http://bar.com/\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/\"},\n\t\t\t},\n\t\t},\n\t\t{ // absolute redirect to deep path with query\n\t\t\troute: \"route add svc / http://bar.com/a/b/c?foo=bar\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/a/b/c?foo=bar\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/a/b/c?foo=bar\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/a/b/c?foo=bar\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/a/b/c?foo=bar\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple http -> https redirect with static path\n\t\t\troute: \"route add redirect *:80/ https://$host/\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"https://foo.com/\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple redirect to corresponding path\n\t\t\troute: \"route add svc / http://bar.com/$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"http://bar.com/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple http -> https redirect to corresponding host & path\n\t\t\troute: \"route add redirect *:80/ https://$host/$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"https://foo.com/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"https://foo.com/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"https://foo.com/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"https://foo.com/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple redirect to corresponding path without / before $path\n\t\t\troute: \"route add svc / http://bar.com$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"http://bar.com/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple http -> https redirect to corresponding host & path without / before $path\n\t\t\troute: \"route add redirect *:80/ https://$host$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"https://foo.com/\"},\n\t\t\t\t{req: \"/abc\", want: \"https://foo.com/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"https://foo.com/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"https://foo.com/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"https://foo.com/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // arbitrary subdir on target with $path at end\n\t\t\troute: \"route add svc / http://bar.com/bbb/$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/bbb/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/bbb/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/bbb/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/bbb/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"http://bar.com/bbb/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // http -> https redir to corresponding host w/ arbitrary subdir on target with $path at end\n\t\t\troute: \"route add redirect *:80/ https://$host/bbb/$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"https://foo.com/bbb/\"},\n\t\t\t\t{req: \"/abc\", want: \"https://foo.com/bbb/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"https://foo.com/bbb/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"https://foo.com/bbb/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"https://foo.com/bbb/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // arbitrary subdir on target with $path at end but without / before $path\n\t\t\troute: \"route add svc / http://bar.com/bbb$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/bbb/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/bbb/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/bbb/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/bbb/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"http://bar.com/bbb/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // http -> https redir to corresponding host w/ arbitrary subdir on target with $path at end but without / before $path\n\t\t\troute: \"route add redirect *:80/ https://$host/bbb$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"https://foo.com/bbb/\"},\n\t\t\t\t{req: \"/abc\", want: \"https://foo.com/bbb/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"https://foo.com/bbb/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"https://foo.com/bbb/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"https://foo.com/bbb/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // simple redirect to corresponding path with encoded char in path\n\t\t\troute: \"route add svc / http://bar.com/$path\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/%20\", want: \"http://bar.com/%20\"},\n\t\t\t\t{req: \"/a%2fbc\", want: \"http://bar.com/a%2fbc\"},\n\t\t\t\t{req: \"/a/b%22/c\", want: \"http://bar.com/a/b%22/c\"},\n\t\t\t\t{req: \"/%2f/?aaa=1\", want: \"http://bar.com/%2f/?aaa=1\"},\n\t\t\t\t{req: \"/%20/a%2f/\", want: \"http://bar.com/%20/a%2f/\"},\n\t\t\t},\n\t\t},\n\t\t{ // strip prefix\n\t\t\troute: \"route add svc /stripme http://bar.com/$path opts \\\"strip=/stripme\\\"\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/stripme/\", want: \"http://bar.com/\"},\n\t\t\t\t{req: \"/stripme/abc\", want: \"http://bar.com/abc\"},\n\t\t\t\t{req: \"/stripme/a/b/c\", want: \"http://bar.com/a/b/c\"},\n\t\t\t\t{req: \"/stripme/?aaa=1\", want: \"http://bar.com/?aaa=1\"},\n\t\t\t\t{req: \"/stripme/abc/?aaa=1\", want: \"http://bar.com/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // strip prefix and redirect #824\n\t\t\troute: \"route add svc *:80/stripme https://bar.com/bbb$path opts \\\"strip=/stripme\\\"\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/stripme\", want: \"https://bar.com/bbb\"},\n\t\t\t\t{req: \"/stripme/abc\", want: \"https://bar.com/bbb/abc\"},\n\t\t\t\t{req: \"/stripme/a/b/c\", want: \"https://bar.com/bbb/a/b/c\"},\n\t\t\t\t{req: \"/stripme/?aaa=1\", want: \"https://bar.com/bbb/?aaa=1\"},\n\t\t\t\t{req: \"/stripme/abc/?aaa=1\", want: \"https://bar.com/bbb/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // prepend prefix\n\t\t\troute: \"route add svc / http://bar.com/$path opts \\\"prepend=/prefix\\\"\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/\", want: \"http://bar.com/prefix/\"},\n\t\t\t\t{req: \"/abc\", want: \"http://bar.com/prefix/abc\"},\n\t\t\t\t{req: \"/a/b/c\", want: \"http://bar.com/prefix/a/b/c\"},\n\t\t\t\t{req: \"/?aaa=1\", want: \"http://bar.com/prefix/?aaa=1\"},\n\t\t\t\t{req: \"/abc/?aaa=1\", want: \"http://bar.com/prefix/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t\t{ // strip & prepend prefix\n\t\t\troute: \"route add svc / http://bar.com/$path opts \\\"prepend=/prefix strip=/stripme\\\"\",\n\t\t\ttests: []routeTest{\n\t\t\t\t{req: \"/stripme/\", want: \"http://bar.com/prefix/\"},\n\t\t\t\t{req: \"/stripme/abc\", want: \"http://bar.com/prefix/abc\"},\n\t\t\t\t{req: \"/stripme/a/b/c\", want: \"http://bar.com/prefix/a/b/c\"},\n\t\t\t\t{req: \"/stripme/?aaa=1\", want: \"http://bar.com/prefix/?aaa=1\"},\n\t\t\t\t{req: \"/stripme/abc/?aaa=1\", want: \"http://bar.com/prefix/abc/?aaa=1\"},\n\t\t\t},\n\t\t},\n\t}\n\tfirstRoute := func(tbl Table) *Route {\n\t\tfor _, routes := range tbl {\n\t\t\treturn routes[0]\n\t\t}\n\t\treturn nil\n\t}\n\tfor _, tt := range tests {\n\t\ttbl, _ := NewTable(bytes.NewBufferString(tt.route))\n\t\troute := firstRoute(tbl)\n\t\ttarget := route.Targets[0]\n\t\tfor _, rt := range tt.tests {\n\t\t\treqURL, _ := url.Parse(\"http://foo.com\" + rt.req)\n\t\t\ttarget.BuildRedirectURL(reqURL)\n\t\t\tif got := target.RedirectURL.String(); got != rt.want {\n\t\t\t\tt.Errorf(\"Got %s, wanted %s\", got, rt.want)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "transport/transport.go",
    "content": "package transport\n\nimport (\n\t\"crypto/tls\"\n\t\"github.com/fabiolb/fabio/config\"\n\t\"net\"\n\t\"net/http\"\n)\n\nvar (\n\tcfg *config.Config = &config.Config{}\n)\n\nfunc NewTransport(tlscfg *tls.Config) *http.Transport {\n\treturn &http.Transport{\n\t\tResponseHeaderTimeout: cfg.Proxy.ResponseHeaderTimeout,\n\t\tIdleConnTimeout:       cfg.Proxy.IdleConnTimeout,\n\t\tMaxIdleConnsPerHost:   cfg.Proxy.MaxConn,\n\t\tDial: (&net.Dialer{\n\t\t\tTimeout:   cfg.Proxy.DialTimeout,\n\t\t\tKeepAlive: cfg.Proxy.KeepAliveTimeout,\n\t\t}).Dial,\n\t\tTLSClientConfig: tlscfg,\n\t}\n}\n\nfunc SetConfig(ncfg *config.Config) {\n\tcfg = ncfg\n}\n"
  },
  {
    "path": "uuid/format.go",
    "content": "package uuid\n\n// Fast UUID formatting adapted from\n// https://github.com/m4rw3r/uuid/blob/master/uuid.go\n\n// halfbyte2hexchar contains an array of character values corresponding to\n// hexadecimal values for the position in the array, 0 to 15 (0x0-0xf, half-byte).\nvar halfbyte2hexchar = []byte{\n\t48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102,\n}\n\n// ToString formats raw UUID bytes as a standard UUID string\nfunc ToString(u [24]byte) string {\n\t/* It is a lot (~10x) faster to allocate a byte slice of specific size and\n\t   then use a lookup table to write the characters to the byte-array and\n\t   finally cast to string instead of using fmt.Sprintf() */\n\t/* Slightly faster to not use make([]byte, 36), guessing either call\n\t   overhead or slice-header overhead is the cause */\n\tb := [36]byte{}\n\n\tfor i, n := range []int{\n\t\t0, 2, 4, 6,\n\t\t9, 11,\n\t\t14, 16,\n\t\t19, 21,\n\t\t24, 26, 28, 30, 32, 34,\n\t} {\n\t\tb[n] = halfbyte2hexchar[(u[i]>>4)&0x0f]\n\t\tb[n+1] = halfbyte2hexchar[u[i]&0x0f]\n\t}\n\n\tb[8] = '-'\n\tb[13] = '-'\n\tb[18] = '-'\n\tb[23] = '-'\n\n\t/* Oddly does not seem to cause a memory allocation,\n\t   internal data-array is most likely just moved over\n\t   to the string-header: */\n\treturn string(b[:])\n}\n"
  },
  {
    "path": "uuid/uuid.go",
    "content": "package uuid\n\nimport (\n\t\"github.com/rogpeppe/fastuuid\"\n)\n\nvar generator = fastuuid.MustNewGenerator()\n\n// NewUUID return UUID in string fromat\nfunc NewUUID() string {\n\treturn ToString(generator.Next())\n}\n"
  }
]