Repository: fabiolb/fabio Branch: master Commit: e7c17a6cfca8 Files: 348 Total size: 1.2 MB Directory structure: gitextract_kam7pb3_/ ├── .dockerignore ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ ├── build.yml │ ├── github-pages.yml │ └── go-releaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile-goreleaser ├── LICENSE ├── Makefile ├── NOTICES.txt ├── README.md ├── admin/ │ ├── api/ │ │ ├── api.go │ │ ├── config.go │ │ ├── manual.go │ │ ├── paths.go │ │ ├── routes.go │ │ └── version.go │ ├── server.go │ ├── server_test.go │ └── ui/ │ ├── assets/ │ │ └── fonts/ │ │ └── material-icons.css │ ├── generate.go │ ├── manual.go │ ├── route.go │ └── static.go ├── assert/ │ └── assert.go ├── auth/ │ ├── auth.go │ ├── auth_test.go │ ├── basic.go │ └── basic_test.go ├── bgp/ │ ├── bgp_nonwindows.go │ ├── bgp_nonwindows_test.go │ ├── bgp_windows.go │ ├── logger.go │ └── test_data/ │ └── bgp.toml ├── build/ │ ├── ca-certificates.crt │ ├── homebrew.sh │ ├── homebrew.vim │ ├── issue-225-gen-cert.bash │ ├── releasenotes.pl │ ├── tag.sh │ └── update-ssl.sh ├── cert/ │ ├── consul_source.go │ ├── consul_source_test.go │ ├── file_source.go │ ├── http_source.go │ ├── load.go │ ├── load_test.go │ ├── path_source.go │ ├── source.go │ ├── source_test.go │ ├── store.go │ ├── store_test.go │ ├── vault_client.go │ ├── vault_pki_source.go │ ├── vault_source.go │ └── watch.go ├── config/ │ ├── config.go │ ├── default.go │ ├── flagset.go │ ├── flagset_test.go │ ├── kvslice.go │ ├── kvslice_test.go │ ├── load.go │ ├── load_test.go │ └── localip.go ├── demo/ │ └── aws/ │ ├── .gitignore │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── docs/ │ ├── .gitignore │ ├── README.md │ ├── archetypes/ │ │ └── default.md │ ├── config.toml │ ├── content/ │ │ ├── _index.md │ │ ├── cfg/ │ │ │ └── _index.md │ │ ├── code-of-conduct/ │ │ │ └── _index.md │ │ ├── contact/ │ │ │ └── _index.md │ │ ├── contrib/ │ │ │ ├── _index.md │ │ │ ├── development.md │ │ │ └── guidelines.md │ │ ├── deploy/ │ │ │ ├── _index.md │ │ │ ├── amazon-api-gw.md │ │ │ ├── amazon-elb.md │ │ │ ├── direct.md │ │ │ └── existing-lb.md │ │ ├── faq/ │ │ │ ├── _index.md │ │ │ ├── binding-to-low-ports.md │ │ │ ├── multiple-protocol-listeners.md │ │ │ ├── request-debugging.md │ │ │ ├── verifying-releases.md │ │ │ └── why-fabio.md │ │ ├── feature/ │ │ │ ├── _index.md │ │ │ ├── access-control.md │ │ │ ├── access-logging.md │ │ │ ├── authorization.md │ │ │ ├── bgp.md │ │ │ ├── certificate-stores.md │ │ │ ├── docker.md │ │ │ ├── dynamic-reloading.md │ │ │ ├── graceful-shutdown.md │ │ │ ├── grpc-proxy.md │ │ │ ├── http-compression.md │ │ │ ├── http-headers.md │ │ │ ├── http-path-prepending.md │ │ │ ├── http-path-stripping.md │ │ │ ├── http-redirects.md │ │ │ ├── https-tcp-sni-proxy.md │ │ │ ├── https-upstream.md │ │ │ ├── metrics.md │ │ │ ├── proxy-protocol.md │ │ │ ├── sse.md │ │ │ ├── tcp-dynamic-proxy.md │ │ │ ├── tcp-proxy.md │ │ │ ├── tcp-sni-proxy.md │ │ │ ├── traffic-shaping.md │ │ │ ├── vault.md │ │ │ ├── web-ui.md │ │ │ └── websockets.md │ │ ├── quickstart/ │ │ │ └── _index.md │ │ └── ref/ │ │ ├── _index.md │ │ ├── bgp.anycastaddresses.md │ │ ├── bgp.asn.md │ │ ├── bgp.certfile.md │ │ ├── bgp.enabled.md │ │ ├── bgp.enablegrpc.md │ │ ├── bgp.gobgpdcfgfile.md │ │ ├── bgp.grpclistenaddress.md │ │ ├── bgp.grpctls.md │ │ ├── bgp.keyfile.md │ │ ├── bgp.listenaddresses.md │ │ ├── bgp.listenport.md │ │ ├── bgp.nexthop.md │ │ ├── bgp.peers.md │ │ ├── bgp.routerid.md │ │ ├── glob.cache.size.md │ │ ├── glob.matching.disabled.md │ │ ├── log.access.format.md │ │ ├── log.access.target.md │ │ ├── log.level.md │ │ ├── log.routes.format.md │ │ ├── metrics.circonus.apiapp.md │ │ ├── metrics.circonus.apikey.md │ │ ├── metrics.circonus.apiurl.md │ │ ├── metrics.circonus.brokerid.md │ │ ├── metrics.circonus.checkid.md │ │ ├── metrics.circonus.submissionurl.md │ │ ├── metrics.dogstatsd.addr.md │ │ ├── metrics.graphite.addr.md │ │ ├── metrics.interval.md │ │ ├── metrics.names.md │ │ ├── metrics.prefix.md │ │ ├── metrics.prometheus.buckets.md │ │ ├── metrics.prometheus.path.md │ │ ├── metrics.prometheus.subsystem.md │ │ ├── metrics.retry.md │ │ ├── metrics.statsd.addr.md │ │ ├── metrics.target.md │ │ ├── metrics.timeout.md │ │ ├── proxy.addr.md │ │ ├── proxy.auth.md │ │ ├── proxy.cs.md │ │ ├── proxy.deregistergraceperiod.md │ │ ├── proxy.dialtimeout.md │ │ ├── proxy.flushinterval.md │ │ ├── proxy.globalflushinterval.md │ │ ├── proxy.grpcmaxrxmsgsize.md │ │ ├── proxy.grpcmaxtxmsgsize.md │ │ ├── proxy.grpcshutdowntimeout.md │ │ ├── proxy.gzip.contenttype.md │ │ ├── proxy.header.clientip.md │ │ ├── proxy.header.requestid.md │ │ ├── proxy.header.sts.maxage.md │ │ ├── proxy.header.sts.preload.md │ │ ├── proxy.header.sts.subdomains.md │ │ ├── proxy.header.tls.md │ │ ├── proxy.header.tls.value.md │ │ ├── proxy.idleconntimeout.md │ │ ├── proxy.keepalivetimeout.md │ │ ├── proxy.localip.md │ │ ├── proxy.matcher.md │ │ ├── proxy.maxconn.md │ │ ├── proxy.noroutestatus.md │ │ ├── proxy.responseheadertimeout.md │ │ ├── proxy.shutdownwait.md │ │ ├── proxy.strategy.md │ │ ├── registry.backend.md │ │ ├── registry.consul.addr.md │ │ ├── registry.consul.checksRequired.md │ │ ├── registry.consul.kvpath.md │ │ ├── registry.consul.namespace.md │ │ ├── registry.consul.noroutehtmlpath.md │ │ ├── registry.consul.pollInterval.md │ │ ├── registry.consul.register.addr.md │ │ ├── registry.consul.register.checkInterval.md │ │ ├── registry.consul.register.checkTLSSkipVerify.md │ │ ├── registry.consul.register.checkTimeout.md │ │ ├── registry.consul.register.deregisterCriticalServiceAfter.md │ │ ├── registry.consul.register.enabled.md │ │ ├── registry.consul.register.name.md │ │ ├── registry.consul.register.tags.md │ │ ├── registry.consul.service.status.md │ │ ├── registry.consul.serviceMonitors.md │ │ ├── registry.consul.tagprefix.md │ │ ├── registry.consul.token.md │ │ ├── registry.custom.checkTLSSkipVerify.md │ │ ├── registry.custom.host.md │ │ ├── registry.custom.path.md │ │ ├── registry.custom.pollinginterval.md │ │ ├── registry.custom.queryparams.md │ │ ├── registry.custom.scheme.md │ │ ├── registry.custom.timeout.md │ │ ├── registry.file.noroutehtmlpath.md │ │ ├── registry.file.path.md │ │ ├── registry.retry.md │ │ ├── registry.static.noroutehtmlpath.md │ │ ├── registry.static.routes.md │ │ ├── registry.timeout.md │ │ ├── runtime.gogc.md │ │ ├── runtime.gomaxprocs.md │ │ ├── ui.access.md │ │ ├── ui.addr.md │ │ ├── ui.color.md │ │ ├── ui.routingtable.source.host.md │ │ ├── ui.routingtable.source.linkenabled.md │ │ ├── ui.routingtable.source.newtab.md │ │ ├── ui.routingtable.source.port.md │ │ ├── ui.routingtable.source.scheme.md │ │ └── ui.title.md │ ├── layouts/ │ │ ├── 404.html │ │ ├── _default/ │ │ │ ├── section.html │ │ │ └── single.html │ │ ├── index.html │ │ └── partials/ │ │ ├── footer.html │ │ ├── header.html │ │ └── sidebar.html │ └── static/ │ ├── CNAME │ ├── check/ │ │ └── ok │ ├── css/ │ │ └── custom.css │ ├── fonts/ │ │ └── FontAwesome.otf │ ├── img/ │ │ └── manifest.json │ └── js/ │ └── custom.js ├── exit/ │ ├── listen.go │ └── listen_test.go ├── fabio.iml ├── fabio.properties ├── go.mod ├── go.sum ├── logger/ │ ├── level_writer.go │ ├── level_writer_test.go │ ├── logger.go │ ├── logger_test.go │ └── pattern.go ├── main.go ├── metrics/ │ ├── metrics.go │ ├── names.go │ ├── names_test.go │ ├── provider_circonus.go │ ├── provider_circonus_test.go │ ├── provider_discard.go │ ├── provider_dogstatsd.go │ ├── provider_dogstatsd_test.go │ ├── provider_flat.go │ ├── provider_graphite.go │ ├── provider_label.go │ ├── provider_multi.go │ ├── provider_prometheus.go │ ├── provider_prometheus_test.go │ ├── provider_statsd.go │ └── provider_statsd_test.go ├── noroute/ │ ├── store.go │ └── store_test.go ├── proxy/ │ ├── grpc_handler.go │ ├── gzip/ │ │ ├── content_type_test.go │ │ ├── gzip_handler.go │ │ └── gzip_handler_test.go │ ├── http_handler.go │ ├── http_headers.go │ ├── http_headers_test.go │ ├── http_integration_test.go │ ├── http_proxy.go │ ├── inetaf_tcpproxy.go │ ├── inetaf_tcpproxy_integration_test.go │ ├── internal/ │ │ └── testcert.go │ ├── listen.go │ ├── listen_test.go │ ├── serve.go │ ├── tcp/ │ │ ├── copy_buffer.go │ │ ├── proxy_proto.go │ │ ├── server.go │ │ ├── sni_proxy.go │ │ ├── tcp_dynamic_proxy.go │ │ ├── tcp_proxy.go │ │ ├── tcptest/ │ │ │ ├── dialer.go │ │ │ └── server.go │ │ ├── tls_clienthello.go │ │ └── tls_clienthello_test.go │ ├── tcp_integration_test.go │ ├── ws_handler.go │ └── ws_integration_test.go ├── registry/ │ ├── backend.go │ ├── consul/ │ │ ├── backend.go │ │ ├── kv.go │ │ ├── passing.go │ │ ├── passing_test.go │ │ ├── register.go │ │ ├── routecmd.go │ │ ├── routecmd_test.go │ │ └── service.go │ ├── custom/ │ │ ├── backend.go │ │ ├── custom.go │ │ └── custom_test.go │ ├── file/ │ │ └── backend.go │ └── static/ │ └── backend.go ├── rootwarn_unix.go ├── rootwarn_windows.go ├── route/ │ ├── access_rules.go │ ├── access_rules_test.go │ ├── auth.go │ ├── auth_test.go │ ├── glob_cache.go │ ├── glob_cache_test.go │ ├── issue57_test.go │ ├── matcher.go │ ├── matcher_test.go │ ├── metrics_cleanup_test.go │ ├── parse_new.go │ ├── parse_test.go │ ├── picker.go │ ├── picker_test.go │ ├── route.go │ ├── route_bench_test.go │ ├── route_def.go │ ├── routes.go │ ├── table.go │ ├── table_registry_test.go │ ├── table_test.go │ ├── target.go │ └── target_test.go ├── transport/ │ └── transport.go └── uuid/ ├── format.go └── uuid.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ fabio dist/ ================================================ FILE: .github/CODEOWNERS ================================================ * @fabiolb/maintainers ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ on: [push, pull_request] name: Build permissions: contents: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: version: v2.5.0 build: runs-on: ubuntu-latest needs: ["golangci"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Install Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' check-latest: true cache: true - name: Set Hosts run: | echo "127.0.0.1 example.com example2.com" | sudo tee -a /etc/hosts - name: Test run: | export PATH=$PATH:$HOME/bin:$HOME/go/bin make github ================================================ FILE: .github/workflows/github-pages.yml ================================================ name: github pages permissions: contents: write on: push: branches: - master jobs: build-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - name: build run: | export PATH=${PATH}:${HOME}/bin make github-pages - name: deploy uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/public cname: fabiolb.net ================================================ FILE: .github/workflows/go-releaser.yml ================================================ name: goreleaser permissions: contents: write on: push: tags: - '*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' check-latest: true cache: true - name: Docker Login uses: docker/login-action@v3 with: registry: https://index.docker.io/v1/ username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} ================================================ FILE: .gitignore ================================================ *-amd64 *.orig *.out *.p12 *.pem *.pprof *.sha256 *.swp *.tar.gz *.test *.un~ *.zip .DS_Store .idea .vagrant build/builds/ fabio fabio.exe fabio.sublime-* demo/cert/ /pkg/ dist/ *.app *.hugo_build.lock *~ .RELEASE.CHANGELOG.md ================================================ FILE: .golangci.yml ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. version: "2" run: concurrency: 4 allow-parallel-runners: true allow-serial-runners: true linters: default: all disable: - bodyclose - containedctx - copyloopvar - cyclop - depguard - dupl - dupword - embeddedstructfieldcheck - err113 - errcheck - errchkjson - errorlint - exhaustive - exhaustruct - forbidigo - forcetypeassert - funcorder - funlen - gochecknoglobals - gochecknoinits - gocognit - goconst - gocritic - gocyclo - godoclint - godot - godox - gosec - govet - ireturn - lll - maintidx - mnd - nakedret - nestif - nilnil - nlreturn - noctx - noinlineerr - nonamedreturns - paralleltest - perfsprint - prealloc - protogetter - revive - tagliatelle - testpackage - thelper - usestdlibvars - usetesting - varnamelen - whitespace - wrapcheck - wsl - wsl_v5 exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - demo/* formatters: exclusions: generated: lax ================================================ FILE: .goreleaser.yml ================================================ version: 2 builds: - binary: fabio env: - CGO_ENABLED=0 goos: - darwin - linux - freebsd - netbsd - openbsd - windows goarch: - 386 - amd64 - arm - arm64 goarm: - 7 archives: - id: bin name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}' formats: - binary source: enabled: true name_template: '{{ .ProjectName }}-{{.Version }}.src' prefix_template: '{{ .ProjectName }}-{{.Version }}/' checksum: name_template: '{{.ProjectName}}-{{.Version}}.sha256' signs: - artifacts: checksum args: - "--batch" - "--local-user" - "{{ .Env.GPG_FINGERPRINT }}" - "--output" - "${signature}" - "--detach-sign" - "${artifact}" dockers: - dockerfile: Dockerfile-goreleaser use: buildx goos: linux goarch: amd64 image_templates: - 'fabiolb/fabio:latest-amd64' - 'fabiolb/fabio:{{ .Version }}-amd64' build_flag_templates: - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - fabio.properties - dockerfile: Dockerfile-goreleaser use: buildx goos: linux goarch: arm64 image_templates: - 'fabiolb/fabio:latest-arm64v8' - 'fabiolb/fabio:{{ .Version }}-arm64v8' build_flag_templates: - "--platform=linux/arm64/v8" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - fabio.properties - dockerfile: Dockerfile-goreleaser use: buildx goos: linux goarch: arm goarm: 7 image_templates: - 'fabiolb/fabio:latest-armv7' - 'fabiolb/fabio:{{ .Version }}-armv7' build_flag_templates: - "--platform=linux/arm/v7" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - fabio.properties docker_manifests: - name_template: 'fabiolb/fabio:latest' image_templates: - 'fabiolb/fabio:latest-amd64' - 'fabiolb/fabio:latest-arm64v8' - 'fabiolb/fabio:latest-armv7' - name_template: 'fabiolb/fabio:{{ .Version }}' image_templates: - 'fabiolb/fabio:{{ .Version }}-amd64' - 'fabiolb/fabio:{{ .Version }}-arm64v8' - 'fabiolb/fabio:{{ .Version }}-armv7' ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [v1.6.10](https://github.com/fabiolb/fabio/tree/v1.6.10) (2025-11-24) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.9...v1.6.10) **Closed issues:** - Call the fatal function within the goroutine of the main test function [\#1009](https://github.com/fabiolb/fabio/issues/1009) - Support for authentication middleware \(eg: oauth2-proxy\) ? [\#1006](https://github.com/fabiolb/fabio/issues/1006) **Merged pull requests:** - Update deps for upstream fixes. [\#1011](https://github.com/fabiolb/fabio/pull/1011) ([tristanmorgan](https://github.com/tristanmorgan)) - Reverse IPv4/IPv6 check. [\#1010](https://github.com/fabiolb/fabio/pull/1010) ([tristanmorgan](https://github.com/tristanmorgan)) ## [v1.6.9](https://github.com/fabiolb/fabio/tree/v1.6.9) (2025-10-16) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.8...v1.6.9) **Merged pull requests:** - FROM scratch build and update deps. [\#1008](https://github.com/fabiolb/fabio/pull/1008) ([tristanmorgan](https://github.com/tristanmorgan)) - feat: add armv7 support for docker images [\#1007](https://github.com/fabiolb/fabio/pull/1007) ([amd989](https://github.com/amd989)) ## [v1.6.8](https://github.com/fabiolb/fabio/tree/v1.6.8) (2025-09-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.7...v1.6.8) **Closed issues:** - Health check on port 9999 [\#995](https://github.com/fabiolb/fabio/issues/995) - Multiple entries in proxy.auth do not work as specified in documentation [\#929](https://github.com/fabiolb/fabio/issues/929) - Correct Consul ACL Policy for Fabio [\#831](https://github.com/fabiolb/fabio/issues/831) - Translating `upstream-host/path` to `path.example.com` [\#801](https://github.com/fabiolb/fabio/issues/801) **Merged pull requests:** - Package dependencies updated. [\#1004](https://github.com/fabiolb/fabio/pull/1004) ([tristanmorgan](https://github.com/tristanmorgan)) - docs: Fixes bad example for creating multiple basic authorization schemes [\#1003](https://github.com/fabiolb/fabio/pull/1003) ([steffkelsey](https://github.com/steffkelsey)) - Enabling many more linters in the pipeline. [\#999](https://github.com/fabiolb/fabio/pull/999) ([tristanmorgan](https://github.com/tristanmorgan)) - Fix missing brand logo in routes page [\#998](https://github.com/fabiolb/fabio/pull/998) ([tristanmorgan](https://github.com/tristanmorgan)) - Fixing up some web links [\#997](https://github.com/fabiolb/fabio/pull/997) ([tristanmorgan](https://github.com/tristanmorgan)) - Fix the ui.routingtable.source.newtab doc. [\#996](https://github.com/fabiolb/fabio/pull/996) ([tristanmorgan](https://github.com/tristanmorgan)) - extract only binary from zipfile [\#994](https://github.com/fabiolb/fabio/pull/994) ([shantanugadgil](https://github.com/shantanugadgil)) - add option to constrain fabio instance to specific consul namespace [\#812](https://github.com/fabiolb/fabio/pull/812) ([baabgai](https://github.com/baabgai)) ## [v1.6.7](https://github.com/fabiolb/fabio/tree/v1.6.7) (2025-05-30) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.6...v1.6.7) **Merged pull requests:** - Bump go.mod pins to latest point release. [\#993](https://github.com/fabiolb/fabio/pull/993) ([tristanmorgan](https://github.com/tristanmorgan)) ## [v1.6.6](https://github.com/fabiolb/fabio/tree/v1.6.6) (2025-05-26) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.5...v1.6.6) **Implemented enhancements:** - Add a golang-linter to CI [\#972](https://github.com/fabiolb/fabio/pull/972) ([aleksraiden](https://github.com/aleksraiden)) **Closed issues:** - wiki content vs fabio/docs/content ? [\#986](https://github.com/fabiolb/fabio/issues/986) **Merged pull requests:** - Using staticcheck to fix many issues. [\#992](https://github.com/fabiolb/fabio/pull/992) ([tristanmorgan](https://github.com/tristanmorgan)) - Update golangci-lint and run yamlfmt. [\#990](https://github.com/fabiolb/fabio/pull/990) ([tristanmorgan](https://github.com/tristanmorgan)) - Fix mistake made in \#988. [\#989](https://github.com/fabiolb/fabio/pull/989) ([tristanmorgan](https://github.com/tristanmorgan)) - Actions permissions [\#988](https://github.com/fabiolb/fabio/pull/988) ([tristanmorgan](https://github.com/tristanmorgan)) - docs: fix broken link [\#987](https://github.com/fabiolb/fabio/pull/987) ([marco-m](https://github.com/marco-m)) - Update dependancies including GoBGP. [\#985](https://github.com/fabiolb/fabio/pull/985) ([tristanmorgan](https://github.com/tristanmorgan)) - Add a CODEOWNERS file. [\#983](https://github.com/fabiolb/fabio/pull/983) ([tristanmorgan](https://github.com/tristanmorgan)) - Document insensitive prefix matching in the list. [\#982](https://github.com/fabiolb/fabio/pull/982) ([tristanmorgan](https://github.com/tristanmorgan)) - Update golang.org/x/net. [\#980](https://github.com/fabiolb/fabio/pull/980) ([tristanmorgan](https://github.com/tristanmorgan)) ## [v1.6.5](https://github.com/fabiolb/fabio/tree/v1.6.5) (2025-02-28) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.4...v1.6.5) **Implemented enhancements:** - Unable to load correct certificates if 1 invalid one is in consul k/v [\#941](https://github.com/fabiolb/fabio/issues/941) - Use a Go 1.24.0 [\#971](https://github.com/fabiolb/fabio/pull/971) ([aleksraiden](https://github.com/aleksraiden)) - Report all certificate errors instead of stopping at the first. \(\#941\) [\#964](https://github.com/fabiolb/fabio/pull/964) ([tristanmorgan](https://github.com/tristanmorgan)) **Closed issues:** - Please bump golang.org/x/sys dependency to enable a build on riscv64-freebsd [\#927](https://github.com/fabiolb/fabio/issues/927) - Fabio is using Datadog reserved tag keys [\#923](https://github.com/fabiolb/fabio/issues/923) **Merged pull requests:** - Update deps to latest [\#975](https://github.com/fabiolb/fabio/pull/975) ([aleksraiden](https://github.com/aleksraiden)) - updating godeps [\#969](https://github.com/fabiolb/fabio/pull/969) ([aleksraiden](https://github.com/aleksraiden)) - Update Hugo config to work with version bump in \#965 [\#967](https://github.com/fabiolb/fabio/pull/967) ([tristanmorgan](https://github.com/tristanmorgan)) - Use Alpine3.21 as base docker image [\#966](https://github.com/fabiolb/fabio/pull/966) ([aleksraiden](https://github.com/aleksraiden)) - update CI components [\#965](https://github.com/fabiolb/fabio/pull/965) ([aleksraiden](https://github.com/aleksraiden)) - 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)) - Update dependancies again. [\#962](https://github.com/fabiolb/fabio/pull/962) ([tristanmorgan](https://github.com/tristanmorgan)) - Use ParseUint to test for overflow directly [\#961](https://github.com/fabiolb/fabio/pull/961) ([dcarbone](https://github.com/dcarbone)) - Fix small typo in DogStatsD config ref. [\#960](https://github.com/fabiolb/fabio/pull/960) ([tristanmorgan](https://github.com/tristanmorgan)) - Rebuild CHANGELOG [\#959](https://github.com/fabiolb/fabio/pull/959) ([tristanmorgan](https://github.com/tristanmorgan)) - Adds handling of Datadog reserved tag keys [\#924](https://github.com/fabiolb/fabio/pull/924) ([froque](https://github.com/froque)) ## [v1.6.4](https://github.com/fabiolb/fabio/tree/v1.6.4) (2024-11-27) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.3...v1.6.4) **Closed issues:** - CI pipeline to run testsuite [\#949](https://github.com/fabiolb/fabio/issues/949) - Fabio not exporting all the metrics with prometheus [\#947](https://github.com/fabiolb/fabio/issues/947) - certificates - cert and ca chain/intermediate [\#946](https://github.com/fabiolb/fabio/issues/946) - This repository is unmaintaned. [\#944](https://github.com/fabiolb/fabio/issues/944) - CVE-2023-44487 HTTP/2 rapid reset [\#939](https://github.com/fabiolb/fabio/issues/939) - TCP no route - cant balance tcp [\#936](https://github.com/fabiolb/fabio/issues/936) - windows: setting logging path in fabio properties [\#920](https://github.com/fabiolb/fabio/issues/920) - Port range in the proxy.addr [\#529](https://github.com/fabiolb/fabio/issues/529) **Merged pull requests:** - Add GoReleaser workflow. [\#958](https://github.com/fabiolb/fabio/pull/958) ([tristanmorgan](https://github.com/tristanmorgan)) - go-kit/kit/log go-kit/log [\#956](https://github.com/fabiolb/fabio/pull/956) ([tristanmorgan](https://github.com/tristanmorgan)) - Update go-retryablehttp to fix warning. [\#954](https://github.com/fabiolb/fabio/pull/954) ([tristanmorgan](https://github.com/tristanmorgan)) - Update and try fix GH Pages publish action. [\#953](https://github.com/fabiolb/fabio/pull/953) ([tristanmorgan](https://github.com/tristanmorgan)) - Update go version, test binaries and package versions. [\#952](https://github.com/fabiolb/fabio/pull/952) ([tristanmorgan](https://github.com/tristanmorgan)) - Remove vendored modules in favour of go mod. [\#951](https://github.com/fabiolb/fabio/pull/951) ([tristanmorgan](https://github.com/tristanmorgan)) - Update Github runner image. [\#950](https://github.com/fabiolb/fabio/pull/950) ([tristanmorgan](https://github.com/tristanmorgan)) - fix: close resp body [\#945](https://github.com/fabiolb/fabio/pull/945) ([testwill](https://github.com/testwill)) - Trim leading and trailing spaces from service tags [\#943](https://github.com/fabiolb/fabio/pull/943) ([logocomune](https://github.com/logocomune)) - Fix doubled download a Vault file [\#942](https://github.com/fabiolb/fabio/pull/942) ([aleksraiden](https://github.com/aleksraiden)) - Remove deprecated ioutil [\#940](https://github.com/fabiolb/fabio/pull/940) ([tristanmorgan](https://github.com/tristanmorgan)) - 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)) - Consul registry performance improvements [\#928](https://github.com/fabiolb/fabio/pull/928) ([ddreier](https://github.com/ddreier)) - \[Docs\] Fix wrong parameter name [\#914](https://github.com/fabiolb/fabio/pull/914) ([KEANO89](https://github.com/KEANO89)) - Updating grpc handler to gracefully close backend connections [\#913](https://github.com/fabiolb/fabio/pull/913) ([nathanejohnson](https://github.com/nathanejohnson)) ## [v1.6.3](https://github.com/fabiolb/fabio/tree/v1.6.3) (2022-12-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.2...v1.6.3) **Implemented enhancements:** - Feature request: Make source links in ui interface clickable [\#901](https://github.com/fabiolb/fabio/issues/901) **Closed issues:** - Ignore host=dst when backend is https [\#916](https://github.com/fabiolb/fabio/issues/916) - poll new feature requests [\#910](https://github.com/fabiolb/fabio/issues/910) - Fabio Clustering. [\#668](https://github.com/fabiolb/fabio/issues/668) **Merged pull requests:** - Disable BGP functionality on Windows since gobgp does not support this. [\#919](https://github.com/fabiolb/fabio/pull/919) ([nathanejohnson](https://github.com/nathanejohnson)) - updating CHANGELOG [\#918](https://github.com/fabiolb/fabio/pull/918) ([nathanejohnson](https://github.com/nathanejohnson)) - 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)) - add feature to advertise anycast addresses via BGP [\#909](https://github.com/fabiolb/fabio/pull/909) ([nathanejohnson](https://github.com/nathanejohnson)) - 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)) - Feature/source link [\#907](https://github.com/fabiolb/fabio/pull/907) ([KTruesdellENA](https://github.com/KTruesdellENA)) ## [v1.6.2](https://github.com/fabiolb/fabio/tree/v1.6.2) (2022-09-13) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.1...v1.6.2) **Closed issues:** - Update TLS cipher parser to include modern ciphers [\#903](https://github.com/fabiolb/fabio/issues/903) - Custom behavior for the situation when the service has no healthy instances [\#898](https://github.com/fabiolb/fabio/issues/898) **Merged pull requests:** - update README for v1.6.2 release [\#905](https://github.com/fabiolb/fabio/pull/905) ([nathanejohnson](https://github.com/nathanejohnson)) - Updating TLS cipher config parser to include TLS 1.3 constants. [\#904](https://github.com/fabiolb/fabio/pull/904) ([nathanejohnson](https://github.com/nathanejohnson)) ## [v1.6.1](https://github.com/fabiolb/fabio/tree/v1.6.1) (2022-07-19) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.6.0...v1.6.1) **Implemented enhancements:** - Multi-DC fabio [\#115](https://github.com/fabiolb/fabio/issues/115) **Fixed bugs:** - 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) **Closed issues:** - admin UI Overrides not working [\#886](https://github.com/fabiolb/fabio/issues/886) - Panic on created prometheus metric name [\#878](https://github.com/fabiolb/fabio/issues/878) - Crash on route update: panic: runtime error: index out of range, diffmatchpatch.\(\*DiffMatchPatch\).DiffCharsToLines [\#873](https://github.com/fabiolb/fabio/issues/873) - Experiencing 502's [\#862](https://github.com/fabiolb/fabio/issues/862) - Fabio immediately drop routes when consul agent unavailable for a while [\#861](https://github.com/fabiolb/fabio/issues/861) - \[proxy/tls\] Update supported TLS versions and cipher suites [\#858](https://github.com/fabiolb/fabio/issues/858) - JSON schema is incorrect in website Dest should be Dst [\#852](https://github.com/fabiolb/fabio/issues/852) - \[question\] URL for TLS destination [\#850](https://github.com/fabiolb/fabio/issues/850) - \[Feature\] Possibility of adding arm/arm64 docker builds. [\#833](https://github.com/fabiolb/fabio/issues/833) **Merged pull requests:** - Release/v1.6.1 [\#897](https://github.com/fabiolb/fabio/pull/897) ([nathanejohnson](https://github.com/nathanejohnson)) - setting sni to match host [\#896](https://github.com/fabiolb/fabio/pull/896) ([KTruesdellENA](https://github.com/KTruesdellENA)) - Update random picker to use math/rand's Intn function [\#893](https://github.com/fabiolb/fabio/pull/893) ([nathanejohnson](https://github.com/nathanejohnson)) - add configurable grpc message sizes to \#632 [\#890](https://github.com/fabiolb/fabio/pull/890) ([nathanejohnson](https://github.com/nathanejohnson)) - add tls13 [\#889](https://github.com/fabiolb/fabio/pull/889) ([nathanejohnson](https://github.com/nathanejohnson)) - update materialize bits. see issue \#886 [\#888](https://github.com/fabiolb/fabio/pull/888) ([nathanejohnson](https://github.com/nathanejohnson)) - Moved admin UI assets to use go embed [\#885](https://github.com/fabiolb/fabio/pull/885) ([nathanejohnson](https://github.com/nathanejohnson)) - update the custom css [\#884](https://github.com/fabiolb/fabio/pull/884) ([KTruesdellENA](https://github.com/KTruesdellENA)) - Bump go-diff dependency version to 1.2.0. Fixes \#873 [\#881](https://github.com/fabiolb/fabio/pull/881) ([nathanejohnson](https://github.com/nathanejohnson)) - bump HUGO version to 0.101.0 [\#880](https://github.com/fabiolb/fabio/pull/880) ([nathanejohnson](https://github.com/nathanejohnson)) - add docs from PR \#854 to fabio.properties [\#879](https://github.com/fabiolb/fabio/pull/879) ([nathanejohnson](https://github.com/nathanejohnson)) - Build multi-arch Docker images for amd64 and arm64 architectures [\#875](https://github.com/fabiolb/fabio/pull/875) ([vamc19](https://github.com/vamc19)) - Fix x-forwarded-for header processing for ws connections [\#860](https://github.com/fabiolb/fabio/pull/860) ([bn0ir](https://github.com/bn0ir)) - Update registry.backend.md [\#854](https://github.com/fabiolb/fabio/pull/854) ([webmutation](https://github.com/webmutation)) - 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)) - 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)) - Add command line flag to toggle required consistency on consul reads [\#811](https://github.com/fabiolb/fabio/pull/811) ([jeremycw](https://github.com/jeremycw)) - Issue 605 grpc host matching [\#632](https://github.com/fabiolb/fabio/pull/632) ([tommyalatalo](https://github.com/tommyalatalo)) ## [v1.6.0](https://github.com/fabiolb/fabio/tree/v1.6.0) (2022-04-11) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.15...v1.6.0) **Implemented enhancements:** - Add support for influxdb metrics [\#253](https://github.com/fabiolb/fabio/issues/253) - Support for prometheus [\#211](https://github.com/fabiolb/fabio/issues/211) - Support dogstatd with tags [\#165](https://github.com/fabiolb/fabio/issues/165) - Riemann metrics support [\#126](https://github.com/fabiolb/fabio/issues/126) - Simple HTTP path prefix replacement [\#767](https://github.com/fabiolb/fabio/pull/767) ([JamesJJ](https://github.com/JamesJJ)) **Closed issues:** - Consul Route updates very slow with large numbers of routes [\#865](https://github.com/fabiolb/fabio/issues/865) - Restricting TLS versions [\#859](https://github.com/fabiolb/fabio/issues/859) - \[admin/ui\] General updates [\#856](https://github.com/fabiolb/fabio/issues/856) - \[question\] - Can Fabio listen on 80/tcp with Nomad [\#844](https://github.com/fabiolb/fabio/issues/844) - Supporting requests in the form of /my-app/page1 [\#842](https://github.com/fabiolb/fabio/issues/842) - Fabio Using Container IPs to create routes [\#839](https://github.com/fabiolb/fabio/issues/839) - All my dynamic routes suddenly vanished! [\#837](https://github.com/fabiolb/fabio/issues/837) - Fabio redirecting to /routes on own service [\#832](https://github.com/fabiolb/fabio/issues/832) - docu fabio configure TLS/SSL\(HTTPS\) understanding problem [\#827](https://github.com/fabiolb/fabio/issues/827) - Crash: \[FATAL\] 1.5.13. strconv.ParseUint: parsing ":1883;PROTO=TCP-DYNAMIC": invalid syntax [\#826](https://github.com/fabiolb/fabio/issues/826) - strip doesn't work as expected on redirect [\#824](https://github.com/fabiolb/fabio/issues/824) - Using Fabio with Consul over mTLS [\#820](https://github.com/fabiolb/fabio/issues/820) - Switch to github actions [\#817](https://github.com/fabiolb/fabio/issues/817) - Panic - httputil: ReverseProxy read error during body copy [\#814](https://github.com/fabiolb/fabio/issues/814) - Support for Consul and Vault Namespaces [\#810](https://github.com/fabiolb/fabio/issues/810) - grpc be closed when uninstall service target [\#807](https://github.com/fabiolb/fabio/issues/807) - fabio binary filename for download [\#805](https://github.com/fabiolb/fabio/issues/805) - Add arm64 arch [\#804](https://github.com/fabiolb/fabio/issues/804) - Can Fabio to prefer one ethernet interface for proxy\_addr? [\#802](https://github.com/fabiolb/fabio/issues/802) - TCP Dynamic Proxy route without specifying exact IP? [\#797](https://github.com/fabiolb/fabio/issues/797) - \[Question\] What are opinions on allowing stale reads of Consul Catalog [\#764](https://github.com/fabiolb/fabio/issues/764) - Simple HTTP path prefix replacement [\#691](https://github.com/fabiolb/fabio/issues/691) - Does Fabio support multiple CS Stores per listener? [\#666](https://github.com/fabiolb/fabio/issues/666) - \[Question\] Stats - Status code per service [\#371](https://github.com/fabiolb/fabio/issues/371) - Statsd output is not good [\#327](https://github.com/fabiolb/fabio/issues/327) - Send metrics to cloudwatch [\#326](https://github.com/fabiolb/fabio/issues/326) - Mixing of HTTPS proxying and SNI+TCP on a single port [\#213](https://github.com/fabiolb/fabio/issues/213) **Merged pull requests:** - gofmt [\#870](https://github.com/fabiolb/fabio/pull/870) ([nathanejohnson](https://github.com/nathanejohnson)) - updating x/sys [\#869](https://github.com/fabiolb/fabio/pull/869) ([nathanejohnson](https://github.com/nathanejohnson)) - update go and alpine versions [\#868](https://github.com/fabiolb/fabio/pull/868) ([Netlims](https://github.com/Netlims)) - \#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)) - removing exclusion of arm64 mac build. Fixes \#804 [\#866](https://github.com/fabiolb/fabio/pull/866) ([nathanejohnson](https://github.com/nathanejohnson)) - Fix example commands in registry.consul.kvpath [\#864](https://github.com/fabiolb/fabio/pull/864) ([blake](https://github.com/blake)) - Add IdleConnTimeout configurable for http transport [\#863](https://github.com/fabiolb/fabio/pull/863) ([aal89](https://github.com/aal89)) - admin/ui updates: [\#857](https://github.com/fabiolb/fabio/pull/857) ([dcarbone](https://github.com/dcarbone)) - Update 2 broken links in documentation [\#822](https://github.com/fabiolb/fabio/pull/822) ([mig4ng](https://github.com/mig4ng)) - Fix small typo in README.md [\#821](https://github.com/fabiolb/fabio/pull/821) ([mig4ng](https://github.com/mig4ng)) - Add support for github actions [\#819](https://github.com/fabiolb/fabio/pull/819) ([nathanejohnson](https://github.com/nathanejohnson)) - Remove golang toolchain name from release binary names [\#818](https://github.com/fabiolb/fabio/pull/818) ([nathanejohnson](https://github.com/nathanejohnson)) - we don't use Fabio [\#813](https://github.com/fabiolb/fabio/pull/813) ([hsmade](https://github.com/hsmade)) - 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)) - Refactor metrics [\#476](https://github.com/fabiolb/fabio/pull/476) ([magiconair](https://github.com/magiconair)) ## [v1.5.15](https://github.com/fabiolb/fabio/tree/v1.5.15) (2020-12-01) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.14...v1.5.15) **Closed issues:** - TCP Dynamic Proxy is not releasing ports from deregistered services [\#796](https://github.com/fabiolb/fabio/issues/796) - How to configure log file output path [\#781](https://github.com/fabiolb/fabio/issues/781) **Merged pull requests:** - 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)) - Stop dynamic TCP listener when upstream is no longer available [\#798](https://github.com/fabiolb/fabio/pull/798) ([fwkz](https://github.com/fwkz)) - Updating dependencies [\#794](https://github.com/fabiolb/fabio/pull/794) ([nathanejohnson](https://github.com/nathanejohnson)) - Update CHANGELOG.md [\#790](https://github.com/fabiolb/fabio/pull/790) ([stevenscg](https://github.com/stevenscg)) ## [v1.5.14](https://github.com/fabiolb/fabio/tree/v1.5.14) (2020-09-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.13...v1.5.14) **Fixed bugs:** - %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) **Closed issues:** - matchingHostNoGlob sometimes returns incorrect matched host [\#786](https://github.com/fabiolb/fabio/issues/786) - Add support for HTTPS+TCP+SNI on the same listener [\#783](https://github.com/fabiolb/fabio/issues/783) - SIGTERM + Gracefully closing connections [\#782](https://github.com/fabiolb/fabio/issues/782) - passing multiple routes via command line [\#776](https://github.com/fabiolb/fabio/issues/776) - Master branch build failing with SECURITY ERROR [\#769](https://github.com/fabiolb/fabio/issues/769) - How to disable client authentication for https? [\#765](https://github.com/fabiolb/fabio/issues/765) - Must Access Control require RemoteAddr matching? [\#754](https://github.com/fabiolb/fabio/issues/754) - Fabio Proxy \(localhost:9999\) Showing Blank White Screen [\#752](https://github.com/fabiolb/fabio/issues/752) - Fabio 1.5.13 - no more "\[INFO\] Config updates" message in the logs [\#751](https://github.com/fabiolb/fabio/issues/751) - Authentication issue. [\#743](https://github.com/fabiolb/fabio/issues/743) - Connecting to HTTPS Upstream service. [\#738](https://github.com/fabiolb/fabio/issues/738) - log.routes.format is broken with 1.5.13 [\#737](https://github.com/fabiolb/fabio/issues/737) - Looking for a new maintainer [\#735](https://github.com/fabiolb/fabio/issues/735) - GRPC Proxy + HTTP Proxy, both useable at the same time? [\#734](https://github.com/fabiolb/fabio/issues/734) - Trace spans all have the same operation name [\#732](https://github.com/fabiolb/fabio/issues/732) - consul: Error fetching config from /fabio/config. Get [\#729](https://github.com/fabiolb/fabio/issues/729) - Very frequent 502 errors [\#721](https://github.com/fabiolb/fabio/issues/721) - Fabio decodes URL path parameters [\#486](https://github.com/fabiolb/fabio/issues/486) - http proxy error context canceled [\#264](https://github.com/fabiolb/fabio/issues/264) **Merged pull requests:** - Fixing issue \#786 - matchingHostNoGlob sometimes returns incorrect host [\#787](https://github.com/fabiolb/fabio/pull/787) ([nathanejohnson](https://github.com/nathanejohnson)) - updating documentation for pending 1.5.14 release [\#785](https://github.com/fabiolb/fabio/pull/785) ([nathanejohnson](https://github.com/nathanejohnson)) - https+tcp+sni listener support [\#784](https://github.com/fabiolb/fabio/pull/784) ([nathanejohnson](https://github.com/nathanejohnson)) - chore: fix typo in comments [\#775](https://github.com/fabiolb/fabio/pull/775) ([josgraha](https://github.com/josgraha)) - \(docs\): fixed small error [\#774](https://github.com/fabiolb/fabio/pull/774) ([0xflotus](https://github.com/0xflotus)) - Preserve table state by storing buffer table in fixed strings. [\#749](https://github.com/fabiolb/fabio/pull/749) ([aaronhurt](https://github.com/aaronhurt)) - only deploy once per build [\#747](https://github.com/fabiolb/fabio/pull/747) ([aaronhurt](https://github.com/aaronhurt)) - switch to github pages for doc hosting [\#746](https://github.com/fabiolb/fabio/pull/746) ([aaronhurt](https://github.com/aaronhurt)) - minor transition updates and small fixes [\#745](https://github.com/fabiolb/fabio/pull/745) ([aaronhurt](https://github.com/aaronhurt)) - switch back to travis CI [\#744](https://github.com/fabiolb/fabio/pull/744) ([nathanejohnson](https://github.com/nathanejohnson)) - follow hugo best practices [\#742](https://github.com/fabiolb/fabio/pull/742) ([aaronhurt](https://github.com/aaronhurt)) - Documentation updates for project transition. [\#740](https://github.com/fabiolb/fabio/pull/740) ([aaronhurt](https://github.com/aaronhurt)) - Fix infinite buffering of SSE responses when gzip is enabled [\#739](https://github.com/fabiolb/fabio/pull/739) ([ctlajoie](https://github.com/ctlajoie)) - Add missing \ entry to example route [\#733](https://github.com/fabiolb/fabio/pull/733) ([BenjaminHerbert](https://github.com/BenjaminHerbert)) - minor fixups [\#731](https://github.com/fabiolb/fabio/pull/731) ([aaronhurt](https://github.com/aaronhurt)) - fix tests [\#730](https://github.com/fabiolb/fabio/pull/730) ([aaronhurt](https://github.com/aaronhurt)) - Add HTTP method and path to trace span operation name [\#715](https://github.com/fabiolb/fabio/pull/715) ([hobochili](https://github.com/hobochili)) - Deprecate deregisterCriticalServiceAfter option [\#674](https://github.com/fabiolb/fabio/pull/674) ([pschultz](https://github.com/pschultz)) - Issue 647 NormalizeHost [\#648](https://github.com/fabiolb/fabio/pull/648) ([murphymj25](https://github.com/murphymj25)) - Handle context canceled errors + better http proxy error handling [\#644](https://github.com/fabiolb/fabio/pull/644) ([danlsgiga](https://github.com/danlsgiga)) - Added idleTimout to config and to serve.go HTTP server [\#635](https://github.com/fabiolb/fabio/pull/635) ([galen0624](https://github.com/galen0624)) - Issue 613 tcp dynamic [\#626](https://github.com/fabiolb/fabio/pull/626) ([murphymj25](https://github.com/murphymj25)) - Issue 554 Added compiled glob matching using LRU Cache [\#615](https://github.com/fabiolb/fabio/pull/615) ([galen0624](https://github.com/galen0624)) - 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)) - Feat: Pass encoded characters in path unchanged [\#489](https://github.com/fabiolb/fabio/pull/489) ([valentin-krasontovitsch](https://github.com/valentin-krasontovitsch)) ## [v1.5.13](https://github.com/fabiolb/fabio/tree/v1.5.13) (2019-11-18) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.12...v1.5.13) **Closed issues:** - Fabio 1.5.12 - panic: runtime error: invalid memory address or nil pointer dereference [\#719](https://github.com/fabiolb/fabio/issues/719) - Question: Load balancing WebSocket connections [\#718](https://github.com/fabiolb/fabio/issues/718) - Question: resources \(css, js files\) by multiple sites [\#717](https://github.com/fabiolb/fabio/issues/717) - Fabio UI not displaying when hit on a DNS name [\#712](https://github.com/fabiolb/fabio/issues/712) - Unable to route to websites [\#676](https://github.com/fabiolb/fabio/issues/676) - Websocket proxy timeouts [\#518](https://github.com/fabiolb/fabio/issues/518) **Merged pull requests:** - fix nil-pointer dereference in detailed config log [\#720](https://github.com/fabiolb/fabio/pull/720) ([pschultz](https://github.com/pschultz)) - Safely handle missing cert from Vault KV store [\#710](https://github.com/fabiolb/fabio/pull/710) ([dradtke](https://github.com/dradtke)) ## [v1.5.12](https://github.com/fabiolb/fabio/tree/v1.5.12) (2019-10-11) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.11...v1.5.12) **Implemented enhancements:** - docker swarm some times register eth0 other eth1 [\#652](https://github.com/fabiolb/fabio/issues/652) - config: let registry.consul.register.addr default to ui.addr [\#658](https://github.com/fabiolb/fabio/pull/658) ([pschultz](https://github.com/pschultz)) - fix exit status code [\#637](https://github.com/fabiolb/fabio/pull/637) ([ianic](https://github.com/ianic)) **Closed issues:** - Example of Vault KV clientca option? [\#703](https://github.com/fabiolb/fabio/issues/703) - tcp proxy not work [\#702](https://github.com/fabiolb/fabio/issues/702) - urlprefix-:3306 proto=tcp not work [\#701](https://github.com/fabiolb/fabio/issues/701) - https proxy not work [\#700](https://github.com/fabiolb/fabio/issues/700) - the http port is 9999 ,the https port is what? [\#699](https://github.com/fabiolb/fabio/issues/699) - TCP proxy log filled with i/o timeout [\#696](https://github.com/fabiolb/fabio/issues/696) - urlprefix-zzz.xxx.com/api not work [\#693](https://github.com/fabiolb/fabio/issues/693) - Fabio/Consul route integration [\#689](https://github.com/fabiolb/fabio/issues/689) - Unable to route. [\#680](https://github.com/fabiolb/fabio/issues/680) - Fabio 100% CPU usage due to logging [\#673](https://github.com/fabiolb/fabio/issues/673) - Authorization header leaking to the backend. [\#671](https://github.com/fabiolb/fabio/issues/671) - X-Request-Start header [\#670](https://github.com/fabiolb/fabio/issues/670) - fabio service entries may stay in Consul on dirty exit [\#663](https://github.com/fabiolb/fabio/issues/663) - Can fabio route request by request body [\#661](https://github.com/fabiolb/fabio/issues/661) - Wrong reported HealthCheck-URI using custom -proxy.addr & -ui.addr [\#657](https://github.com/fabiolb/fabio/issues/657) - Clarify documentation HTTP Redirects [\#656](https://github.com/fabiolb/fabio/issues/656) - tcp access control doesn't work [\#651](https://github.com/fabiolb/fabio/issues/651) - Crash on start of watchBackend\(\) [\#650](https://github.com/fabiolb/fabio/issues/650) - Remove third-party cookie and script requirements from frontend [\#642](https://github.com/fabiolb/fabio/issues/642) - Build should use included vendor directory with modules [\#638](https://github.com/fabiolb/fabio/issues/638) - Route table UI is broken [\#628](https://github.com/fabiolb/fabio/issues/628) - Possible Memory Leak in WatchBackend [\#595](https://github.com/fabiolb/fabio/issues/595) - Release date for 1.5.11 [\#592](https://github.com/fabiolb/fabio/issues/592) - Fabio and Vault Token Issues [\#523](https://github.com/fabiolb/fabio/issues/523) - UI broken where no internet access. [\#502](https://github.com/fabiolb/fabio/issues/502) - make log compatible with the syslog protocol [\#397](https://github.com/fabiolb/fabio/issues/397) **Merged pull requests:** - Add Vault example to the traffic shaping section. [\#677](https://github.com/fabiolb/fabio/pull/677) ([jrasell](https://github.com/jrasell)) - Fix matching priority for host:port tuples [\#675](https://github.com/fabiolb/fabio/pull/675) ([pschultz](https://github.com/pschultz)) - Add config option to use 128 bit trace IDs [\#669](https://github.com/fabiolb/fabio/pull/669) ([gfloyd](https://github.com/gfloyd)) - register: clean-up fabio service entries in Consul on dirty exit [\#664](https://github.com/fabiolb/fabio/pull/664) ([pires](https://github.com/pires)) - Fix SSE by implementing Flusher in responseWriter wrapper [\#655](https://github.com/fabiolb/fabio/pull/655) ([gfloyd](https://github.com/gfloyd)) - Use go-sockaddr to parse address strings [\#653](https://github.com/fabiolb/fabio/pull/653) ([aaronhurt](https://github.com/aaronhurt)) - ensure absolute path after strip to maintain rfc complaince [\#645](https://github.com/fabiolb/fabio/pull/645) ([aaronhurt](https://github.com/aaronhurt)) - Bundle UI assets [\#643](https://github.com/fabiolb/fabio/pull/643) ([pschultz](https://github.com/pschultz)) - ui: Remove duplicate destination column [\#641](https://github.com/fabiolb/fabio/pull/641) ([pschultz](https://github.com/pschultz)) - use vendor directory when building - fixes \#638 [\#639](https://github.com/fabiolb/fabio/pull/639) ([aaronhurt](https://github.com/aaronhurt)) - Issue 595 watchbackend [\#629](https://github.com/fabiolb/fabio/pull/629) ([murphymj25](https://github.com/murphymj25)) - added support for profile/tracing [\#624](https://github.com/fabiolb/fabio/pull/624) ([galen0624](https://github.com/galen0624)) ## [v1.5.11](https://github.com/fabiolb/fabio/tree/v1.5.11) (2019-04-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.11-wrong...v1.5.11) **Implemented enhancements:** - Proxy protocol support fo outgoing connections [\#191](https://github.com/fabiolb/fabio/issues/191) **Closed issues:** - Consul blocking queries should be rate limited to avoid spiking loads on server [\#627](https://github.com/fabiolb/fabio/issues/627) - This seems to be a recursive func call. Is this correct? [\#625](https://github.com/fabiolb/fabio/issues/625) - Bug in consul 1.4.3 [\#616](https://github.com/fabiolb/fabio/issues/616) - \[question\] Release date for 1.5.11 [\#601](https://github.com/fabiolb/fabio/issues/601) - Sidebar of the website is a little off [\#599](https://github.com/fabiolb/fabio/issues/599) - wrong use function strings.HasPrefix\(\) in file passsing.go [\#545](https://github.com/fabiolb/fabio/issues/545) - best way to bypass fabio consul integration? [\#437](https://github.com/fabiolb/fabio/issues/437) **Merged pull requests:** - Issue 611 Added Custom API Driven Back end [\#614](https://github.com/fabiolb/fabio/pull/614) ([galen0624](https://github.com/galen0624)) - Improved basic auth htpasswd file refresh \#604 [\#610](https://github.com/fabiolb/fabio/pull/610) ([mfuterko](https://github.com/mfuterko)) - Address \#545 - wrong use function strings.HasPrefix [\#607](https://github.com/fabiolb/fabio/pull/607) ([mfuterko](https://github.com/mfuterko)) - docs: fix layout without JS enabled [\#606](https://github.com/fabiolb/fabio/pull/606) ([pschultz](https://github.com/pschultz)) - Implement basic auth htpasswd file refresh [\#604](https://github.com/fabiolb/fabio/pull/604) ([mfuterko](https://github.com/mfuterko)) - added support for Consul TLS transport [\#602](https://github.com/fabiolb/fabio/pull/602) ([sev3ryn](https://github.com/sev3ryn)) - 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)) ## [v1.5.11-wrong](https://github.com/fabiolb/fabio/tree/v1.5.11-wrong) (2019-02-25) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.10...v1.5.11-wrong) **Implemented enhancements:** - Basic authentication on routes [\#166](https://github.com/fabiolb/fabio/issues/166) **Fixed bugs:** - TCP proxy broken since v1.5.8 [\#524](https://github.com/fabiolb/fabio/issues/524) **Closed issues:** - Fabio's routing table empty. Consul indicates registered services with urlprefix- tags [\#589](https://github.com/fabiolb/fabio/issues/589) - HTTP 502 response half of the time [\#584](https://github.com/fabiolb/fabio/issues/584) - tcp+sni route with allow=ip:something does not seem to work [\#576](https://github.com/fabiolb/fabio/issues/576) - Passing args to fabio in nomad task. [\#567](https://github.com/fabiolb/fabio/issues/567) - Change Log entry update [\#562](https://github.com/fabiolb/fabio/issues/562) - Release date for 1.5.10? [\#560](https://github.com/fabiolb/fabio/issues/560) - Route updates are delayed with large number of services [\#558](https://github.com/fabiolb/fabio/issues/558) - could the source and destination be clickable in the ui? [\#508](https://github.com/fabiolb/fabio/issues/508) - Support for opentracing [\#429](https://github.com/fabiolb/fabio/issues/429) - Case-insensitive path matching [\#35](https://github.com/fabiolb/fabio/issues/35) **Merged pull requests:** - ui: Fix XSS vulnerability [\#588](https://github.com/fabiolb/fabio/pull/588) ([pschultz](https://github.com/pschultz)) - make Dest column into clickable links [\#587](https://github.com/fabiolb/fabio/pull/587) ([kneufeld](https://github.com/kneufeld)) - update documentation around the changes to PROXY protocol [\#583](https://github.com/fabiolb/fabio/pull/583) ([aaronhurt](https://github.com/aaronhurt)) - address concerns raised while troubleshooting \#524 [\#581](https://github.com/fabiolb/fabio/pull/581) ([aaronhurt](https://github.com/aaronhurt)) - fix ip access rules within tcp proxy - fixes \#576 [\#577](https://github.com/fabiolb/fabio/pull/577) ([aaronhurt](https://github.com/aaronhurt)) - Add GRPC proxy support [\#575](https://github.com/fabiolb/fabio/pull/575) ([andyroyle](https://github.com/andyroyle)) - metrics.circonus: Add support for circonus.submissionurl [\#574](https://github.com/fabiolb/fabio/pull/574) ([stack72](https://github.com/stack72)) - add http-basic auth reading from a file [\#573](https://github.com/fabiolb/fabio/pull/573) ([andyroyle](https://github.com/andyroyle)) - update consul to v1.4.0 - fixes \#569 [\#571](https://github.com/fabiolb/fabio/pull/571) ([aaronhurt](https://github.com/aaronhurt)) - add faq to address \#490 [\#568](https://github.com/fabiolb/fabio/pull/568) ([aaronhurt](https://github.com/aaronhurt)) - Update go.mod for \#472 [\#565](https://github.com/fabiolb/fabio/pull/565) ([magiconair](https://github.com/magiconair)) - Refactor consul service monitor [\#564](https://github.com/fabiolb/fabio/pull/564) ([magiconair](https://github.com/magiconair)) - issue 562 update change log glob.matching.disabled [\#563](https://github.com/fabiolb/fabio/pull/563) ([galen0624](https://github.com/galen0624)) - Added new case insensitive matcher [\#553](https://github.com/fabiolb/fabio/pull/553) ([herbrandson](https://github.com/herbrandson)) - \[Docs\] Delete duplicate 'Path Stripping' page [\#537](https://github.com/fabiolb/fabio/pull/537) ([rkettelerij](https://github.com/rkettelerij)) - \#429 issue - OpenTrace zipKin Support [\#472](https://github.com/fabiolb/fabio/pull/472) ([galen0624](https://github.com/galen0624)) ## [v1.5.10](https://github.com/fabiolb/fabio/tree/v1.5.10) (2018-10-25) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.9...v1.5.10) **Fixed bugs:** - Wrong route for multiple matching host glob patterns [\#506](https://github.com/fabiolb/fabio/issues/506) **Closed issues:** - Fabio forcing response header keys upper case [\#552](https://github.com/fabiolb/fabio/issues/552) - Multiple fabio instances load balancing different set of services. [\#551](https://github.com/fabiolb/fabio/issues/551) - Without Consul how can i use Fabio? [\#549](https://github.com/fabiolb/fabio/issues/549) - Performance issue - Glob matching with large number of services in consul [\#548](https://github.com/fabiolb/fabio/issues/548) - Ignore host case when adding and matching routes [\#542](https://github.com/fabiolb/fabio/issues/542) - allow redirect host to be empty [\#533](https://github.com/fabiolb/fabio/issues/533) - Expose Fabio metrics via Prometheus [\#532](https://github.com/fabiolb/fabio/issues/532) - Memory leak in go-metrics library [\#530](https://github.com/fabiolb/fabio/issues/530) - ability to remove headers from the request [\#528](https://github.com/fabiolb/fabio/issues/528) - urlprefix- does not work properly [\#527](https://github.com/fabiolb/fabio/issues/527) - Problem geting fabio routing to its own ui [\#525](https://github.com/fabiolb/fabio/issues/525) - Redirection to default back-end if route not exists [\#521](https://github.com/fabiolb/fabio/issues/521) - Forwarding Uri tag in original request to endpoint [\#519](https://github.com/fabiolb/fabio/issues/519) - Fabio - Manual overrides [\#515](https://github.com/fabiolb/fabio/issues/515) - 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) - Fabio instantly delete route, whereas health check is passing [\#512](https://github.com/fabiolb/fabio/issues/512) - Would it be possible to configure Fabio to watch services with warning state? [\#509](https://github.com/fabiolb/fabio/issues/509) - Headers passed through fabio are modified [\#505](https://github.com/fabiolb/fabio/issues/505) - Fabio -\> HTTPS -\> Service ? [\#503](https://github.com/fabiolb/fabio/issues/503) - Tls + sni support for non http traffic? [\#499](https://github.com/fabiolb/fabio/issues/499) - Static routes in fabio.properties [\#498](https://github.com/fabiolb/fabio/issues/498) - Tests fail with consul \> 1.0.6 and vault \> 0.9.6 [\#494](https://github.com/fabiolb/fabio/issues/494) - Question: wildcard hostname support [\#491](https://github.com/fabiolb/fabio/issues/491) - Fabio confi help with multiple proto [\#490](https://github.com/fabiolb/fabio/issues/490) - Add support for Vault 0.10 KV v2 [\#483](https://github.com/fabiolb/fabio/issues/483) - Support "standard" Consul envvars [\#277](https://github.com/fabiolb/fabio/issues/277) - Support Consul TLS [\#276](https://github.com/fabiolb/fabio/issues/276) **Merged pull requests:** - Issue \#548 added enable/disable glob matching [\#550](https://github.com/fabiolb/fabio/pull/550) ([galen0624](https://github.com/galen0624)) - Correct the access control feature documentation page [\#546](https://github.com/fabiolb/fabio/pull/546) ([msvbhat](https://github.com/msvbhat)) - Add $host pseudo variable [\#544](https://github.com/fabiolb/fabio/pull/544) ([holtwilkins](https://github.com/holtwilkins)) - compare host using lowercase [\#543](https://github.com/fabiolb/fabio/pull/543) ([shantanugadgil](https://github.com/shantanugadgil)) - Issue \#530 - Vendored in updated go-metrics package [\#535](https://github.com/fabiolb/fabio/pull/535) ([galen0624](https://github.com/galen0624)) - Add setting to flush fabio buffer regardless headers [\#531](https://github.com/fabiolb/fabio/pull/531) ([samm-git](https://github.com/samm-git)) - Update README.md [\#510](https://github.com/fabiolb/fabio/pull/510) ([kuskmen](https://github.com/kuskmen)) - Issue \#506: reverse domain names before sorting [\#507](https://github.com/fabiolb/fabio/pull/507) ([magiconair](https://github.com/magiconair)) - Fix changelog link in docs footer [\#500](https://github.com/fabiolb/fabio/pull/500) ([xmikus01](https://github.com/xmikus01)) - Make tests compatible with Vault 0.10 [\#497](https://github.com/fabiolb/fabio/pull/497) ([pschultz](https://github.com/pschultz)) - Delete an unused global variable logOutput [\#495](https://github.com/fabiolb/fabio/pull/495) ([gua-pian](https://github.com/gua-pian)) - Add fastcgi handler [\#435](https://github.com/fabiolb/fabio/pull/435) ([Gufran](https://github.com/Gufran)) ## [v1.5.9](https://github.com/fabiolb/fabio/tree/v1.5.9) (2018-05-16) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.8...v1.5.9) **Closed issues:** - UI is broken from versions =\> 1.7 [\#487](https://github.com/fabiolb/fabio/issues/487) - Building master fails [\#482](https://github.com/fabiolb/fabio/issues/482) - '-registry.consul.register.enabled' does not seem to be respected [\#467](https://github.com/fabiolb/fabio/issues/467) - Access logging fails in combination with proxy gzipping [\#460](https://github.com/fabiolb/fabio/issues/460) - glob matching improvements [\#452](https://github.com/fabiolb/fabio/issues/452) - Add route based on x-forwarded-port header [\#450](https://github.com/fabiolb/fabio/issues/450) - Redirect http to https on the same destination [\#448](https://github.com/fabiolb/fabio/issues/448) - WebSocket Upgrade not sending Response [\#447](https://github.com/fabiolb/fabio/issues/447) - Fabio does not remove service when one of the registered health-checks fail [\#427](https://github.com/fabiolb/fabio/issues/427) - Fabio routing to wrong back end [\#421](https://github.com/fabiolb/fabio/issues/421) - \[feature\]: proxy route option [\#356](https://github.com/fabiolb/fabio/issues/356) **Merged pull requests:** - Resetting read deadline [\#492](https://github.com/fabiolb/fabio/pull/492) ([craigday](https://github.com/craigday)) - Issue \#466: make redirect code more robust [\#477](https://github.com/fabiolb/fabio/pull/477) ([magiconair](https://github.com/magiconair)) - fix contributors link [\#475](https://github.com/fabiolb/fabio/pull/475) ([aaronhurt](https://github.com/aaronhurt)) - ws close on failed handshake \(\#421\) [\#474](https://github.com/fabiolb/fabio/pull/474) ([magiconair](https://github.com/magiconair)) - Issue \#460: Fix access logging when gzip is enabled [\#470](https://github.com/fabiolb/fabio/pull/470) ([magiconair](https://github.com/magiconair)) - Fix the regex of the example proxy.gzip.contenttype [\#468](https://github.com/fabiolb/fabio/pull/468) ([tino](https://github.com/tino)) - Check upstream X-Forwarded-Proto prior to redirect [\#466](https://github.com/fabiolb/fabio/pull/466) ([aaronhurt](https://github.com/aaronhurt)) - Fix certificate stores doc path [\#458](https://github.com/fabiolb/fabio/pull/458) ([eldondev](https://github.com/eldondev)) - Add new & improved glob matcher [\#457](https://github.com/fabiolb/fabio/pull/457) ([sharbov](https://github.com/sharbov)) - handle indeterminate length proxy chains - fixes \#449 [\#453](https://github.com/fabiolb/fabio/pull/453) ([aaronhurt](https://github.com/aaronhurt)) - Update link for Websockets [\#446](https://github.com/fabiolb/fabio/pull/446) ([a2ar](https://github.com/a2ar)) - "strict" health-checking \(\#427\) [\#428](https://github.com/fabiolb/fabio/pull/428) ([systemfreund](https://github.com/systemfreund)) ## [v1.5.8](https://github.com/fabiolb/fabio/tree/v1.5.8) (2018-02-18) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.7...v1.5.8) **Closed issues:** - TCP Proxying SSH connections [\#445](https://github.com/fabiolb/fabio/issues/445) - route add ... opts "proto=tcp+sni" ?? [\#444](https://github.com/fabiolb/fabio/issues/444) - Wildcard registeration issues [\#440](https://github.com/fabiolb/fabio/issues/440) - Feature Request: IP Whitelisting [\#439](https://github.com/fabiolb/fabio/issues/439) - NoRouteHTMLPath not rendering HTML page [\#438](https://github.com/fabiolb/fabio/issues/438) **Merged pull requests:** - ignore fabio.exe [\#443](https://github.com/fabiolb/fabio/pull/443) ([aaronhurt](https://github.com/aaronhurt)) - Issue \#438: Do not add separators for NoRouteHTML page [\#441](https://github.com/fabiolb/fabio/pull/441) ([magiconair](https://github.com/magiconair)) - 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)) - TCP+SNI support arbitrary large Client Hello [\#423](https://github.com/fabiolb/fabio/pull/423) ([DanSipola](https://github.com/DanSipola)) ## [v1.5.7](https://github.com/fabiolb/fabio/tree/v1.5.7) (2018-02-06) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.6...v1.5.7) **Closed issues:** - VaultPKI tests fail with go1.10rc1 [\#434](https://github.com/fabiolb/fabio/issues/434) - Ensure that proxy.noroutestatus has three digits [\#433](https://github.com/fabiolb/fabio/issues/433) - Vault PKI documentation and Fabio version [\#430](https://github.com/fabiolb/fabio/issues/430) - configure equivalent of nginx client\_max\_body\_size [\#422](https://github.com/fabiolb/fabio/issues/422) - \[question\] Newbie question: where to place urlpref-host/path? [\#419](https://github.com/fabiolb/fabio/issues/419) - Static / Manual routes management via API [\#396](https://github.com/fabiolb/fabio/issues/396) - Warn if fabio is run as root [\#369](https://github.com/fabiolb/fabio/issues/369) **Merged pull requests:** - Activating Open Collective [\#432](https://github.com/fabiolb/fabio/pull/432) ([monkeywithacupcake](https://github.com/monkeywithacupcake)) - fix small typo [\#431](https://github.com/fabiolb/fabio/pull/431) ([aaronhurt](https://github.com/aaronhurt)) - 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)) - Fix maxconn documentation [\#420](https://github.com/fabiolb/fabio/pull/420) ([slobo](https://github.com/slobo)) - treat registry.consul.kvpath as prefix [\#417](https://github.com/fabiolb/fabio/pull/417) ([magiconair](https://github.com/magiconair)) - Issue \#369: Do not allow to run fabio as root [\#377](https://github.com/fabiolb/fabio/pull/377) ([magiconair](https://github.com/magiconair)) ## [v1.5.6](https://github.com/fabiolb/fabio/tree/v1.5.6) (2018-01-05) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.5...v1.5.6) **Closed issues:** - Excessive consul logging [\#408](https://github.com/fabiolb/fabio/issues/408) - Build new website [\#405](https://github.com/fabiolb/fabio/issues/405) - \[bug?\] Fabio uses "global" Consul ServiceID's [\#383](https://github.com/fabiolb/fabio/issues/383) **Merged pull requests:** - Issue \#408: log consul state changes as DEBUG [\#418](https://github.com/fabiolb/fabio/pull/418) ([magiconair](https://github.com/magiconair)) - Actually respect -version option [\#415](https://github.com/fabiolb/fabio/pull/415) ([pschultz](https://github.com/pschultz)) - Identify services using both the ID and the Node [\#414](https://github.com/fabiolb/fabio/pull/414) ([alvaroaleman](https://github.com/alvaroaleman)) ## [v1.5.5](https://github.com/fabiolb/fabio/tree/v1.5.5) (2017-12-20) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.4...v1.5.5) **Implemented enhancements:** - Support custom 404/503 error pages [\#56](https://github.com/fabiolb/fabio/issues/56) **Closed issues:** - Fabio for task/container/service load balancing on amazon ecs with consul and registrator. [\#402](https://github.com/fabiolb/fabio/issues/402) **Merged pull requests:** - Implement custom noroute html response [\#398](https://github.com/fabiolb/fabio/pull/398) ([tino](https://github.com/tino)) ## [v1.5.4](https://github.com/fabiolb/fabio/tree/v1.5.4) (2017-12-10) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.3...v1.5.4) **Implemented enhancements:** - Differentiate "URL Unavailable/503" and "URL Not Found/404" [\#214](https://github.com/fabiolb/fabio/issues/214) **Fixed bugs:** - opts with host= with multiple routes does not work as expected [\#385](https://github.com/fabiolb/fabio/issues/385) **Closed issues:** - Fabio is not handling SIGHUP \(HUP\) signal properly - it dies [\#400](https://github.com/fabiolb/fabio/issues/400) - Typo in manual overrides stops Fabio from updating routes [\#399](https://github.com/fabiolb/fabio/issues/399) - route precendence [\#389](https://github.com/fabiolb/fabio/issues/389) - how to connect consul cluster [\#386](https://github.com/fabiolb/fabio/issues/386) - Allow comments in manual overrides [\#379](https://github.com/fabiolb/fabio/issues/379) - Domain or protocol redirection [\#87](https://github.com/fabiolb/fabio/issues/87) - Should rewrite the Host Header [\#75](https://github.com/fabiolb/fabio/issues/75) **Merged pull requests:** - Issue \#400: ignore SIGHUP [\#403](https://github.com/fabiolb/fabio/pull/403) ([magiconair](https://github.com/magiconair)) - Issue \#389: match exact host before glob matches [\#390](https://github.com/fabiolb/fabio/pull/390) ([magiconair](https://github.com/magiconair)) - Issue \#385: attach options to target instead of route [\#388](https://github.com/fabiolb/fabio/pull/388) ([magiconair](https://github.com/magiconair)) - Fix various minor things [\#382](https://github.com/fabiolb/fabio/pull/382) ([antham](https://github.com/antham)) - Remove unused variable [\#381](https://github.com/fabiolb/fabio/pull/381) ([antham](https://github.com/antham)) - 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)) ## [v1.5.3](https://github.com/fabiolb/fabio/tree/v1.5.3) (2017-11-03) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.2...v1.5.3) **Implemented enhancements:** - Drop privileges after start [\#195](https://github.com/fabiolb/fabio/issues/195) - support for adding CORS headers? [\#110](https://github.com/fabiolb/fabio/issues/110) **Closed issues:** - host=www.mydomain.com not working [\#375](https://github.com/fabiolb/fabio/issues/375) - Wildcards in routing path [\#374](https://github.com/fabiolb/fabio/issues/374) - Questions/issues in using overrides [\#372](https://github.com/fabiolb/fabio/issues/372) - nodes and services in maintenance can cause excessive logging [\#367](https://github.com/fabiolb/fabio/issues/367) - Support fabio.properties in Consul KV store [\#365](https://github.com/fabiolb/fabio/issues/365) - 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) - More than one fabio instance decreases system performance. [\#361](https://github.com/fabiolb/fabio/issues/361) - Documentation of the available metrics? [\#360](https://github.com/fabiolb/fabio/issues/360) - select color scheme from config to distinguish environments [\#359](https://github.com/fabiolb/fabio/issues/359) - \[Feature request\]: TCP Proxy support different incoming and outbound ports [\#353](https://github.com/fabiolb/fabio/issues/353) - hgfiii [\#351](https://github.com/fabiolb/fabio/issues/351) - statsd - unable to parse line - gf metric [\#350](https://github.com/fabiolb/fabio/issues/350) - Possibility for Docker Image to pass Consul IP and Port as Variable? [\#346](https://github.com/fabiolb/fabio/issues/346) - Ways to have log verbosity [\#345](https://github.com/fabiolb/fabio/issues/345) - Cant disable consul register with -registry.consul.register.enabled=false [\#342](https://github.com/fabiolb/fabio/issues/342) - Glob Matcher is not working for me [\#341](https://github.com/fabiolb/fabio/issues/341) - Strip option has no effect for websockets [\#330](https://github.com/fabiolb/fabio/issues/330) - access logging is not right [\#322](https://github.com/fabiolb/fabio/issues/322) - FATAL error when metrics cannot be delivered [\#320](https://github.com/fabiolb/fabio/issues/320) - http: proxy error: context canceled [\#318](https://github.com/fabiolb/fabio/issues/318) - /api/routes intermittently returns null. [\#316](https://github.com/fabiolb/fabio/issues/316) - what is the tcp writeTimeout? [\#307](https://github.com/fabiolb/fabio/issues/307) **Merged pull requests:** - Issue \#375: set host header when host option is set [\#376](https://github.com/fabiolb/fabio/pull/376) ([magiconair](https://github.com/magiconair)) ## [v1.5.2](https://github.com/fabiolb/fabio/tree/v1.5.2) (2017-07-24) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.1...v1.5.2) **Implemented enhancements:** - Auto-generated Vault certs [\#135](https://github.com/fabiolb/fabio/issues/135) **Closed issues:** - not able to acces the service via fabio. [\#319](https://github.com/fabiolb/fabio/issues/319) **Merged pull requests:** - Fix memory leak in tcp proxy [\#321](https://github.com/fabiolb/fabio/pull/321) ([Crypto89](https://github.com/Crypto89)) ## [v1.5.1](https://github.com/fabiolb/fabio/tree/v1.5.1) (2017-07-06) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.5.0...v1.5.1) **Implemented enhancements:** - Feature: Allow weight tag in Consul [\#42](https://github.com/fabiolb/fabio/issues/42) **Fixed bugs:** - 1.5.0 config compatibility problem [\#305](https://github.com/fabiolb/fabio/issues/305) **Closed issues:** - Multiple urlprefix [\#317](https://github.com/fabiolb/fabio/issues/317) - Add metrics for TCP and TCP+SNI proxy [\#306](https://github.com/fabiolb/fabio/issues/306) - How to configure TCP correctly \(proxy.addr, ...\) [\#283](https://github.com/fabiolb/fabio/issues/283) - Add parameter to vault token renewal [\#274](https://github.com/fabiolb/fabio/issues/274) **Merged pull requests:** - Issue \#274: Avoid premature vault token renewals [\#314](https://github.com/fabiolb/fabio/pull/314) ([pschultz](https://github.com/pschultz)) - Make tests work with vault 0.7.x [\#313](https://github.com/fabiolb/fabio/pull/313) ([pschultz](https://github.com/pschultz)) - Fix syntax highlighting in README [\#311](https://github.com/fabiolb/fabio/pull/311) ([agis](https://github.com/agis)) ## [v1.5.0](https://github.com/fabiolb/fabio/tree/v1.5.0) (2017-06-07) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.4...v1.5.0) **Implemented enhancements:** - X-Forwarded-Prefix header support [\#304](https://github.com/fabiolb/fabio/issues/304) - read only web ui [\#302](https://github.com/fabiolb/fabio/issues/302) - Sync X-Forwarded-Proto and Forwarded header when possible [\#296](https://github.com/fabiolb/fabio/issues/296) - Using upstream hostname for request [\#294](https://github.com/fabiolb/fabio/issues/294) - Add profiling support [\#290](https://github.com/fabiolb/fabio/issues/290) - TLS and Connection information through headers [\#280](https://github.com/fabiolb/fabio/issues/280) - Support TLS/Ciphersuite configuration options [\#249](https://github.com/fabiolb/fabio/issues/249) **Fixed bugs:** - Support gzip compression for websockets [\#300](https://github.com/fabiolb/fabio/issues/300) **Closed issues:** - Example of proxy.gzip.contenttype configuration [\#299](https://github.com/fabiolb/fabio/issues/299) - Compatibility with 1.8 [\#297](https://github.com/fabiolb/fabio/issues/297) - cert file names and path= not working as documented [\#293](https://github.com/fabiolb/fabio/issues/293) - Multiple SSL certs for same listener [\#291](https://github.com/fabiolb/fabio/issues/291) - HTTPProxy cannot be aware of timeout of waiting response [\#288](https://github.com/fabiolb/fabio/issues/288) - websockets failing with 500 response - running rancher behind fabio [\#133](https://github.com/fabiolb/fabio/issues/133) **Merged pull requests:** - Using upstream hostname for request \(\#294\) [\#301](https://github.com/fabiolb/fabio/pull/301) ([mitchelldavis](https://github.com/mitchelldavis)) ## [v1.4.4](https://github.com/fabiolb/fabio/tree/v1.4.4) (2017-05-08) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.3...v1.4.4) **Implemented enhancements:** - Add service name to access log fields [\#278](https://github.com/fabiolb/fabio/issues/278) **Fixed bugs:** - Fabio does not advertise http/1.1 on TLS connections [\#289](https://github.com/fabiolb/fabio/issues/289) - fabio does not start with multiple listen sockets [\#279](https://github.com/fabiolb/fabio/issues/279) - Websocket not working with HTTPS Upstream [\#271](https://github.com/fabiolb/fabio/issues/271) **Closed issues:** - Reload configuration without restarting fabio by SIGHUP or by flag. [\#286](https://github.com/fabiolb/fabio/issues/286) - chunked Transfer-Encoding [\#284](https://github.com/fabiolb/fabio/issues/284) - How to know what opts are supported in a route / consul tag? [\#270](https://github.com/fabiolb/fabio/issues/270) - Question: Support for Consul v0.7.3 Node tags [\#252](https://github.com/fabiolb/fabio/issues/252) ## [v1.4.3](https://github.com/fabiolb/fabio/tree/v1.4.3) (2017-04-24) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.2...v1.4.3) **Fixed bugs:** - Access log cannot be disabled [\#269](https://github.com/fabiolb/fabio/issues/269) **Closed issues:** - Can fabio proxy by hostname? [\#267](https://github.com/fabiolb/fabio/issues/267) - Issues with Haproxy on passthrough mode [\#266](https://github.com/fabiolb/fabio/issues/266) - How to configure HTTPS upstream manually with tlsskipverify [\#260](https://github.com/fabiolb/fabio/issues/260) - HTTPS upstream added as HTTP [\#259](https://github.com/fabiolb/fabio/issues/259) **Merged pull requests:** - Add support for TLSSkipVerify for https consul fabio check [\#268](https://github.com/fabiolb/fabio/pull/268) ([Ginja](https://github.com/Ginja)) ## [v1.4.2](https://github.com/fabiolb/fabio/tree/v1.4.2) (2017-04-10) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4.1...v1.4.2) **Implemented enhancements:** - Add HTTPS upstream support [\#181](https://github.com/fabiolb/fabio/issues/181) **Closed issues:** - Find the route across the machine, but no response [\#256](https://github.com/fabiolb/fabio/issues/256) **Merged pull requests:** - Allow UI/API to be served over https [\#258](https://github.com/fabiolb/fabio/pull/258) ([tmessi](https://github.com/tmessi)) - Add https upstream support [\#257](https://github.com/fabiolb/fabio/pull/257) ([tmessi](https://github.com/tmessi)) ## [v1.4.1](https://github.com/fabiolb/fabio/tree/v1.4.1) (2017-04-04) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4...v1.4.1) **Implemented enhancements:** - Add generic TCP proxying support [\#179](https://github.com/fabiolb/fabio/issues/179) - Add tests and timeouts to TCP+SNI proxy [\#178](https://github.com/fabiolb/fabio/issues/178) **Closed issues:** - Is there any option to enable HSTS [\#254](https://github.com/fabiolb/fabio/issues/254) ## [v1.4](https://github.com/fabiolb/fabio/tree/v1.4) (2017-03-25) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4rc1...v1.4) ## [v1.4rc1](https://github.com/fabiolb/fabio/tree/v1.4rc1) (2017-03-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4beta2...v1.4rc1) ## [v1.4beta2](https://github.com/fabiolb/fabio/tree/v1.4beta2) (2017-03-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.4beta1...v1.4beta2) ## [v1.4beta1](https://github.com/fabiolb/fabio/tree/v1.4beta1) (2017-03-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.8...v1.4beta1) **Implemented enhancements:** - Start listener after routing table is initialized [\#248](https://github.com/fabiolb/fabio/issues/248) - Support glob host matching [\#163](https://github.com/fabiolb/fabio/issues/163) - Refactor urlprefix tags [\#111](https://github.com/fabiolb/fabio/issues/111) - TCP proxying support [\#1](https://github.com/fabiolb/fabio/issues/1) **Closed issues:** - feature idea: fabio can be configured to only serve consul services with certain tags [\#245](https://github.com/fabiolb/fabio/issues/245) - How does services get in to router table of fabio [\#237](https://github.com/fabiolb/fabio/issues/237) ## [v1.3.8](https://github.com/fabiolb/fabio/tree/v1.3.8) (2017-02-14) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.7...v1.3.8) **Implemented enhancements:** - Retry registry during startup [\#240](https://github.com/fabiolb/fabio/issues/240) - Make route update logging format configurable [\#238](https://github.com/fabiolb/fabio/issues/238) - Support absolute URLs [\#219](https://github.com/fabiolb/fabio/issues/219) **Fixed bugs:** - requests and notfound metric missing [\#218](https://github.com/fabiolb/fabio/issues/218) - fabio 1.3.6 UI displays host and path as 'undefined' in the routes page [\#217](https://github.com/fabiolb/fabio/issues/217) **Closed issues:** - https support [\#241](https://github.com/fabiolb/fabio/issues/241) - Fabio - setup details [\#235](https://github.com/fabiolb/fabio/issues/235) - Not able to connect to fabio UI ... I wonder if I miss any specifics ?. [\#234](https://github.com/fabiolb/fabio/issues/234) - Error in Fabio setup on container where consul-agent \(client\) is installed [\#233](https://github.com/fabiolb/fabio/issues/233) - Fabio Connecting error to local consul-agent \(client\) [\#232](https://github.com/fabiolb/fabio/issues/232) - Load balancing between multiple service cluster nodes [\#231](https://github.com/fabiolb/fabio/issues/231) - Specify Consul service name in Fabio config [\#230](https://github.com/fabiolb/fabio/issues/230) - caching [\#228](https://github.com/fabiolb/fabio/issues/228) - Links in docs to the Traffic Shaping page are dead [\#222](https://github.com/fabiolb/fabio/issues/222) - Overrides API and GUI save KV Store as wrong name [\#220](https://github.com/fabiolb/fabio/issues/220) ## [v1.3.7](https://github.com/fabiolb/fabio/tree/v1.3.7) (2017-01-19) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.6...v1.3.7) **Implemented enhancements:** - Support deleting routes by tag [\#201](https://github.com/fabiolb/fabio/issues/201) **Fixed bugs:** - Fabio does not serve http2 with go \>= 1.7 [\#215](https://github.com/fabiolb/fabio/issues/215) - Bad statsd mean metric format [\#207](https://github.com/fabiolb/fabio/issues/207) **Closed issues:** - Fabio is not able to pick service from consul and not able to update routing table. [\#210](https://github.com/fabiolb/fabio/issues/210) ## [v1.3.6](https://github.com/fabiolb/fabio/tree/v1.3.6) (2017-01-17) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.5...v1.3.6) **Implemented enhancements:** - Refactor config loader tests [\#199](https://github.com/fabiolb/fabio/issues/199) - Routing by path [\#164](https://github.com/fabiolb/fabio/issues/164) - Strip prefix in the forwarded request [\#44](https://github.com/fabiolb/fabio/issues/44) **Fixed bugs:** - runtime error: integer divide by zero [\#186](https://github.com/fabiolb/fabio/issues/186) **Closed issues:** - fabio proxy for consul not work, log show no route [\#212](https://github.com/fabiolb/fabio/issues/212) - Consul registration won't disable [\#209](https://github.com/fabiolb/fabio/issues/209) - Fabio hangs for 30+ seconds for 204 response [\#206](https://github.com/fabiolb/fabio/issues/206) - Fabio running using Nomad system scheduler breaks Docker. [\#192](https://github.com/fabiolb/fabio/issues/192) ## [v1.3.5](https://github.com/fabiolb/fabio/tree/v1.3.5) (2016-11-30) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.4...v1.3.5) **Implemented enhancements:** - fabio --version switch should work just like -v [\#197](https://github.com/fabiolb/fabio/issues/197) - Remove proxy.header.tls header from inbound request [\#194](https://github.com/fabiolb/fabio/issues/194) - Support transparent response body compression [\#119](https://github.com/fabiolb/fabio/issues/119) **Fixed bugs:** - missing 'cs' in map [\#189](https://github.com/fabiolb/fabio/issues/189) - WebSockets not working with IE10 - header casing. [\#183](https://github.com/fabiolb/fabio/issues/183) - Vault CA Certificate [\#182](https://github.com/fabiolb/fabio/issues/182) **Closed issues:** - Logs request [\#188](https://github.com/fabiolb/fabio/issues/188) - Is this the expecting behavior of Fabio with paths? [\#187](https://github.com/fabiolb/fabio/issues/187) - TCP+SNI support on the same port as HTTPS [\#169](https://github.com/fabiolb/fabio/issues/169) ## [v1.3.4](https://github.com/fabiolb/fabio/tree/v1.3.4) (2016-10-28) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.3...v1.3.4) ## [v1.3.3](https://github.com/fabiolb/fabio/tree/v1.3.3) (2016-10-12) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.2...v1.3.3) **Implemented enhancements:** - Provide linux/arm and linux/arm64 binaries [\#161](https://github.com/fabiolb/fabio/issues/161) - Metrics Prefix with templates [\#160](https://github.com/fabiolb/fabio/pull/160) ([md2k](https://github.com/md2k)) **Fixed bugs:** - TCP+SNI proxy does not work with PROXY protocol [\#177](https://github.com/fabiolb/fabio/issues/177) - Consul cert store URL with token not parsed correctly [\#172](https://github.com/fabiolb/fabio/issues/172) - Panic on invalid response [\#159](https://github.com/fabiolb/fabio/issues/159) **Closed issues:** - can not see new application added to the same fabio instance [\#176](https://github.com/fabiolb/fabio/issues/176) - Ridiculous lack for docker documentation [\#175](https://github.com/fabiolb/fabio/issues/175) - OT: logo for the eBay organization [\#158](https://github.com/fabiolb/fabio/issues/158) **Merged pull requests:** - Use Go's net.JoinHostPort which will auto-detect ipv6 [\#167](https://github.com/fabiolb/fabio/pull/167) ([jovandeginste](https://github.com/jovandeginste)) ## [v1.3.2](https://github.com/fabiolb/fabio/tree/v1.3.2) (2016-09-11) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3.1...v1.3.2) **Fixed bugs:** - ParseListen may set the wrong protocol [\#157](https://github.com/fabiolb/fabio/issues/157) ## [v1.3.1](https://github.com/fabiolb/fabio/tree/v1.3.1) (2016-09-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.3...v1.3.1) ## [v1.3](https://github.com/fabiolb/fabio/tree/v1.3) (2016-09-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2.1...v1.3) **Implemented enhancements:** - Add support for Circonus metrics [\#151](https://github.com/fabiolb/fabio/issues/151) - Support multiple metrics libraries [\#147](https://github.com/fabiolb/fabio/issues/147) - Is there a way to prevent SSL requests falling back to an unrelated cert? [\#138](https://github.com/fabiolb/fabio/issues/138) - Vault token should not require 'root' or 'sudo' privileges [\#134](https://github.com/fabiolb/fabio/issues/134) - Extended metrics [\#125](https://github.com/fabiolb/fabio/issues/125) **Fixed bugs:** - fabio fails to start with "\[FATAL\] 1.2. missing 'cs' in cs" [\#146](https://github.com/fabiolb/fabio/issues/146) **Closed issues:** - fabio g-rpc [\#156](https://github.com/fabiolb/fabio/issues/156) - Routing based on Accept Header [\#155](https://github.com/fabiolb/fabio/issues/155) - not all command-line options seem to do anything [\#152](https://github.com/fabiolb/fabio/issues/152) ## [v1.2.1](https://github.com/fabiolb/fabio/tree/v1.2.1) (2016-08-25) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2...v1.2.1) **Implemented enhancements:** - Server-sent events support [\#129](https://github.com/fabiolb/fabio/issues/129) - access logging [\#80](https://github.com/fabiolb/fabio/issues/80) - Support configuration via command line arguments [\#79](https://github.com/fabiolb/fabio/issues/79) - Support statsd [\#73](https://github.com/fabiolb/fabio/issues/73) - SSL Certs from Vault [\#70](https://github.com/fabiolb/fabio/issues/70) - Refactor listener config [\#28](https://github.com/fabiolb/fabio/issues/28) - Add/remove certificates using API [\#27](https://github.com/fabiolb/fabio/issues/27) **Fixed bugs:** - Always deregister from Consul [\#136](https://github.com/fabiolb/fabio/issues/136) **Closed issues:** - HA access to the management interface on instances [\#145](https://github.com/fabiolb/fabio/issues/145) - Fabio is not adding route, but health check is passing [\#142](https://github.com/fabiolb/fabio/issues/142) - Wrong Destination IP [\#140](https://github.com/fabiolb/fabio/issues/140) - Having trouble recognizing routes from consul [\#137](https://github.com/fabiolb/fabio/issues/137) **Merged pull requests:** - Improve error message on missing trailing slash [\#143](https://github.com/fabiolb/fabio/pull/143) ([juliangamble](https://github.com/juliangamble)) - added statsd support [\#139](https://github.com/fabiolb/fabio/pull/139) ([jshaw86](https://github.com/jshaw86)) ## [v1.2](https://github.com/fabiolb/fabio/tree/v1.2) (2016-07-16) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc4...v1.2) **Fixed bugs:** - fabio 1.2rc3 panics with -v [\#128](https://github.com/fabiolb/fabio/issues/128) ## [v1.2rc4](https://github.com/fabiolb/fabio/tree/v1.2rc4) (2016-07-13) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc3...v1.2rc4) ## [v1.2rc3](https://github.com/fabiolb/fabio/tree/v1.2rc3) (2016-07-12) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.6...v1.2rc3) ## [v1.1.6](https://github.com/fabiolb/fabio/tree/v1.1.6) (2016-07-12) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc2...v1.1.6) **Implemented enhancements:** - TLS handshake error: failed to verify client's certificate [\#108](https://github.com/fabiolb/fabio/issues/108) **Fixed bugs:** - X-Forwarded-Port should use local port [\#122](https://github.com/fabiolb/fabio/issues/122) **Closed issues:** - Path problem [\#124](https://github.com/fabiolb/fabio/issues/124) ## [v1.2rc2](https://github.com/fabiolb/fabio/tree/v1.2rc2) (2016-06-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.5...v1.2rc2) ## [v1.1.5](https://github.com/fabiolb/fabio/tree/v1.1.5) (2016-06-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.2rc1...v1.1.5) **Implemented enhancements:** - Allow routes to a service in warning status [\#117](https://github.com/fabiolb/fabio/pull/117) ([erikvanoosten](https://github.com/erikvanoosten)) **Closed issues:** - Fabio hangs for 30+ seconds for 204 response [\#120](https://github.com/fabiolb/fabio/issues/120) ## [v1.2rc1](https://github.com/fabiolb/fabio/tree/v1.2rc1) (2016-06-15) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.4...v1.2rc1) ## [v1.1.4](https://github.com/fabiolb/fabio/tree/v1.1.4) (2016-06-15) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3...v1.1.4) **Implemented enhancements:** - Custom status code when no route found [\#107](https://github.com/fabiolb/fabio/issues/107) - Keep fabio registered in consul [\#100](https://github.com/fabiolb/fabio/issues/100) - Disable fabio health check in consul [\#99](https://github.com/fabiolb/fabio/issues/99) - Support PROXY protocol [\#97](https://github.com/fabiolb/fabio/issues/97) **Closed issues:** - fabio should expose a /health endpoint [\#112](https://github.com/fabiolb/fabio/issues/112) - Go 1.5 issue [\#109](https://github.com/fabiolb/fabio/issues/109) ## [v1.1.3](https://github.com/fabiolb/fabio/tree/v1.1.3) (2016-05-19) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3rc2...v1.1.3) **Implemented enhancements:** - Keep sort order in UI stable [\#104](https://github.com/fabiolb/fabio/issues/104) - Trim whitespace around tag [\#103](https://github.com/fabiolb/fabio/issues/103) - SNI support? [\#85](https://github.com/fabiolb/fabio/issues/85) ## [v1.1.3rc2](https://github.com/fabiolb/fabio/tree/v1.1.3rc2) (2016-05-14) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.3rc1...v1.1.3rc2) **Implemented enhancements:** - Add glob path matching \(an alternative to default prefix matching\) [\#93](https://github.com/fabiolb/fabio/pull/93) ([dkong](https://github.com/dkong)) ## [v1.1.3rc1](https://github.com/fabiolb/fabio/tree/v1.1.3rc1) (2016-05-09) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.2...v1.1.3rc1) **Implemented enhancements:** - Improve forward headers [\#98](https://github.com/fabiolb/fabio/issues/98) - Allow tags for fabio service registration [\#96](https://github.com/fabiolb/fabio/issues/96) - Expand experimental HTTP API [\#95](https://github.com/fabiolb/fabio/issues/95) - Drop default port from request [\#90](https://github.com/fabiolb/fabio/issues/90) - Use Address instead of ServiceAddress? [\#88](https://github.com/fabiolb/fabio/issues/88) - Expand ${DC} to consul datacenter [\#55](https://github.com/fabiolb/fabio/issues/55) **Closed issues:** - proxy handler error channel bug? [\#92](https://github.com/fabiolb/fabio/issues/92) ## [v1.1.2](https://github.com/fabiolb/fabio/tree/v1.1.2) (2016-04-27) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1.1...v1.1.2) **Fixed bugs:** - Deleted routes hide visible routes [\#57](https://github.com/fabiolb/fabio/issues/57) **Closed issues:** - Recommended way to bind multiple fabio instances to public IP for HA [\#89](https://github.com/fabiolb/fabio/issues/89) - Windows support [\#86](https://github.com/fabiolb/fabio/issues/86) - How to load balance '/'? [\#83](https://github.com/fabiolb/fabio/issues/83) - register websockets with consul tags [\#82](https://github.com/fabiolb/fabio/issues/82) - fabio does not respect registry\_consul\_register\_ip from ENV [\#77](https://github.com/fabiolb/fabio/issues/77) - Not deregistering when consul health status fails [\#71](https://github.com/fabiolb/fabio/issues/71) - question: configure through environment variables? [\#68](https://github.com/fabiolb/fabio/issues/68) - support middleware\(OWIN\) to execute some code before recirection [\#64](https://github.com/fabiolb/fabio/issues/64) **Merged pull requests:** - \#77 fix documentaion [\#78](https://github.com/fabiolb/fabio/pull/78) ([sielaq](https://github.com/sielaq)) - Expose the docker ports in Dockerfile [\#76](https://github.com/fabiolb/fabio/pull/76) ([smancke](https://github.com/smancke)) - Overworked header handling. [\#74](https://github.com/fabiolb/fabio/pull/74) ([smancke](https://github.com/smancke)) - Broken link corrected. [\#65](https://github.com/fabiolb/fabio/pull/65) ([jest](https://github.com/jest)) ## [v1.1.1](https://github.com/fabiolb/fabio/tree/v1.1.1) (2016-02-22) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1...v1.1.1) **Merged pull requests:** - Fix use of local ip in consul service registration [\#58](https://github.com/fabiolb/fabio/pull/58) ([jeanblanchard](https://github.com/jeanblanchard)) ## [v1.1](https://github.com/fabiolb/fabio/tree/v1.1) (2016-02-18) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.1rc1...v1.1) **Implemented enhancements:** - Make read and write timeout configurable [\#53](https://github.com/fabiolb/fabio/issues/53) ## [v1.1rc1](https://github.com/fabiolb/fabio/tree/v1.1rc1) (2016-02-15) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.9...v1.1rc1) ## [v1.0.9](https://github.com/fabiolb/fabio/tree/v1.0.9) (2016-02-15) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.8...v1.0.9) **Implemented enhancements:** - Allow configuration of serviceip used during consul registration [\#48](https://github.com/fabiolb/fabio/issues/48) - Allow configuration via env vars [\#43](https://github.com/fabiolb/fabio/issues/43) - Cleanup metrics for deleted routes [\#41](https://github.com/fabiolb/fabio/issues/41) - HTTP2 support with latest Go [\#32](https://github.com/fabiolb/fabio/issues/32) - Support additional backends [\#12](https://github.com/fabiolb/fabio/issues/12) **Fixed bugs:** - Include services with check ids other than 'service:\*' [\#29](https://github.com/fabiolb/fabio/issues/29) **Closed issues:** - Move dependencies to vendor path [\#47](https://github.com/fabiolb/fabio/issues/47) - Add support for Consul ACL token to demo server [\#37](https://github.com/fabiolb/fabio/issues/37) ## [v1.0.8](https://github.com/fabiolb/fabio/tree/v1.0.8) (2016-01-14) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.7...v1.0.8) **Implemented enhancements:** - Consul ACL Token [\#36](https://github.com/fabiolb/fabio/issues/36) **Fixed bugs:** - Detect when consul agent is down [\#26](https://github.com/fabiolb/fabio/issues/26) - fabio route not removed after consul deregister [\#22](https://github.com/fabiolb/fabio/issues/22) **Closed issues:** - Session persistence [\#33](https://github.com/fabiolb/fabio/issues/33) - Build fails on master/last release tag [\#31](https://github.com/fabiolb/fabio/issues/31) - Documentation: make build before running ./fabio [\#24](https://github.com/fabiolb/fabio/issues/24) **Merged pull requests:** - \[registry\] fallback to given local IP address [\#30](https://github.com/fabiolb/fabio/pull/30) ([doublerebel](https://github.com/doublerebel)) ## [v1.0.7](https://github.com/fabiolb/fabio/tree/v1.0.7) (2015-12-13) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.6...v1.0.7) **Fixed bugs:** - routes not removed when passing empty string [\#23](https://github.com/fabiolb/fabio/issues/23) **Closed issues:** - server demo: Consul health check fails [\#21](https://github.com/fabiolb/fabio/issues/21) - Demo \(shebang, documentation\) [\#20](https://github.com/fabiolb/fabio/issues/20) - \(Docker\) Error initializing backend. [\#19](https://github.com/fabiolb/fabio/issues/19) ## [v1.0.6](https://github.com/fabiolb/fabio/tree/v1.0.6) (2015-12-01) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.5...v1.0.6) **Implemented enhancements:** - Filter routing table not on tags [\#16](https://github.com/fabiolb/fabio/issues/16) - Support websockets [\#9](https://github.com/fabiolb/fabio/issues/9) **Fixed bugs:** - Traffic shaping does not match on service name [\#15](https://github.com/fabiolb/fabio/issues/15) **Closed issues:** - Manage manual overrides via UI [\#18](https://github.com/fabiolb/fabio/issues/18) **Merged pull requests:** - README: fix typos [\#14](https://github.com/fabiolb/fabio/pull/14) ([ceh](https://github.com/ceh)) ## [v1.0.5](https://github.com/fabiolb/fabio/tree/v1.0.5) (2015-11-11) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.4...v1.0.5) **Implemented enhancements:** - Support Forwarded and X-Forwarded-For headers [\#10](https://github.com/fabiolb/fabio/issues/10) **Merged pull requests:** - fix vet warning [\#13](https://github.com/fabiolb/fabio/pull/13) ([juliendsv](https://github.com/juliendsv)) ## [v1.0.4](https://github.com/fabiolb/fabio/tree/v1.0.4) (2015-11-03) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.3...v1.0.4) **Implemented enhancements:** - Support SSL/TLS client cert authentication [\#8](https://github.com/fabiolb/fabio/issues/8) **Closed issues:** - List among Consul community tools [\#6](https://github.com/fabiolb/fabio/issues/6) **Merged pull requests:** - Fixes broken fragment identifier link [\#11](https://github.com/fabiolb/fabio/pull/11) ([budnik](https://github.com/budnik)) ## [v1.0.3](https://github.com/fabiolb/fabio/tree/v1.0.3) (2015-10-26) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.2...v1.0.3) **Merged pull requests:** - Correcting a typo [\#5](https://github.com/fabiolb/fabio/pull/5) ([mdevreugd](https://github.com/mdevreugd)) ## [v1.0.2](https://github.com/fabiolb/fabio/tree/v1.0.2) (2015-10-23) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.1...v1.0.2) **Merged pull requests:** - Honor consul.url and consul.addr from config file [\#3](https://github.com/fabiolb/fabio/pull/3) ([jeinwag](https://github.com/jeinwag)) ## [v1.0.1](https://github.com/fabiolb/fabio/tree/v1.0.1) (2015-10-21) [Full Changelog](https://github.com/fabiolb/fabio/compare/v1.0.0...v1.0.1) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct Please respect others and treat them the way you want to be treated yourself. If you feel someone overstepped please reach out to one of the [project owners](https://github.com/orgs/fabiolb/teams/owners) and someone will follow up. Thank you The Fabio Team ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines More information on how to contribute to fabio can be found on the [wiki](https://github.com/fabiolb/fabio/wiki/Contributing) ## Financial contributions We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/fabio). Anyone 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. ## Credits ### Contributors Thank you to all the people who have already contributed to fabio! ### Backers Thank you to all our backers! [[Become a backer](https://opencollective.com/fabio#backer)] ### Sponsors Thank 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)) ================================================ FILE: Dockerfile ================================================ FROM golang AS build ARG TARGETARCH ARG consul_version=1.22.0 ADD https://releases.hashicorp.com/consul/${consul_version}/consul_${consul_version}_linux_${TARGETARCH}.zip /usr/local/bin RUN cd /usr/local/bin && unzip consul_${consul_version}_linux_${TARGETARCH}.zip consul ARG vault_version=1.21.0 ADD https://releases.hashicorp.com/vault/${vault_version}/vault_${vault_version}_linux_${TARGETARCH}.zip /usr/local/bin RUN cd /usr/local/bin && unzip vault_${vault_version}_linux_${TARGETARCH}.zip vault RUN apt-get update && apt-get install -y git ca-certificates libcap2-bin WORKDIR /src COPY . . RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags "-s -w" -o /src/fabio RUN setcap cap_net_bind_service=+ep /src/fabio RUN echo "nobody:x:65534:65534:nobody:/:/sbin/nologin" > /passwd RUN echo "nogroup:x:65533:" > /group FROM scratch COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /src/fabio /usr/bin/ COPY --from=build /passwd /etc/ COPY --from=build /group /etc/ ADD --chown=nobody:nogroup fabio.properties /etc/fabio/fabio.properties USER nobody:nogroup EXPOSE 9998 9999 ENTRYPOINT ["/usr/bin/fabio"] CMD ["-cfg", "/etc/fabio/fabio.properties"] ================================================ FILE: Dockerfile-goreleaser ================================================ FROM debian:stable-slim AS build RUN apt-get update && apt-get install -y git ca-certificates libcap2-bin ADD fabio /usr/bin/ RUN setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/fabio RUN echo "nobody:x:65534:65534:nobody:/:/sbin/nologin" > /passwd RUN echo "nogroup:x:65533:" > /group FROM scratch COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /usr/bin/fabio /usr/bin/ COPY --from=build /passwd /etc/ COPY --from=build /group /etc/ ADD --chown=nobody:nogroup fabio.properties /etc/fabio/fabio.properties USER nobody:nogroup EXPOSE 9998 9999 ENTRYPOINT ["/usr/bin/fabio"] CMD ["-cfg", "/etc/fabio/fabio.properties"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 Education Networks of America. All rights reserved. Copyright (c) 2017-2020 Frank Schroeder. All rights reserved. (after 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870) Copyright (c) 2015-2017 eBay Software Foundation. All rights reserved. (before 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # CUR_TAG is the last git tag plus the delta from the current commit to the tag # e.g. v1.5.5--g CUR_TAG ?= $(shell git describe --tags --first-parent) # LAST_TAG is the last git tag # e.g. v1.5.5 LAST_TAG ?= $(shell git describe --tags --first-parent --abbrev=0) # VERSION is the last git tag without the 'v' # e.g. 1.5.5 VERSION ?= $(shell git describe --tags --first-parent --abbrev=0 | cut -c 2-) # GOFLAGS is the flags for the go compiler. GOFLAGS ?= -ldflags "-X main.version=$(CUR_TAG)" # GOVERSION is the current go version, e.g. go1.9.2 GOVERSION ?= $(shell go version | awk '{print $$3;}') # GORELEASER is the path to the goreleaser binary. GORELEASER ?= $(shell which goreleaser) # pin versions for CI builds CI_CONSUL_VERSION ?= 1.22.0 CI_VAULT_VERSION ?= 1.21.0 CI_HUGO_VERSION ?= 0.142.0 CI_GOBGP_VERSION ?= 3.37.0 BETA_OSES = linux darwin # all is the default target all: test # help prints a help screen help: @echo "generate - go generate (use it when updating admin ui assets)" @echo "build - go build" @echo "install - go install" @echo "test - go test" @echo "gofmt - go fmt" @echo "linux - go build linux/amd64" @echo "release - tag, build and publish release with goreleaser" @echo "pkg - build, test and create pkg/fabio.tar.gz" @echo "clean-adm - remove admin ui assets" @echo "clean - remove temp files" @echo "clean-all - execute all clean commands" # generate executes all go:generate statements .PHONY: generate generate: clean-adm go generate $(GOFLAGS) ./... # build compiles fabio and the test dependencies .PHONY: build build: gofmt go build $(GOFLAGS) # test builds and runs the tests .PHONY: test test: build go test $(GOFLAGS) -v -test.timeout 15s ./... # mod performs go module maintenance .PHONY: mod mod: go mod tidy # gofmt runs gofmt on the code .PHONY: gofmt gofmt: gofmt -s -w `find . -type f -name '*.go'` beta: $(BETA_OSES) beta: sha256sum fabio_$(CUR_TAG)_*_amd64 > fabio_$(CUR_TAG).sha256 fabio.properties gpg -b fabio_$(CUR_TAG).sha256 tar czvf fabio_$(CUR_TAG).tar.gz fabio.properties fabio_$(CUR_TAG)_*_amd64 fabio_$(CUR_TAG).sha256 fabio_$(CUR_TAG).sha256.sig $(BETA_OSES): CGO_ENABLED=0 GOOS=$@ GOARCH=amd64 go build -trimpath -tags netgo $(GOFLAGS) -o fabio_$(CUR_TAG)_$@_amd64 # install runs go install .PHONY: install install: CGO_ENABLED=0 go install -trimpath $(GOFLAGS) # pkg builds a fabio.tar.gz package with only fabio in it .PHONY: pkg pkg: build test rm -rf pkg mkdir pkg tar czf pkg/fabio.tar.gz fabio # release tags, builds and publishes a build with goreleaser # # Run this in sub-shells instead of dependencies so that # later targets can pick up the new tag value. .PHONY: release release: $(MAKE) tag $(MAKE) preflight docker-test gorelease homebrew # preflight runs some checks before a release .PHONY: preflight preflight: [ "$(CUR_TAG)" == "$(LAST_TAG)" ] || ( echo "master not tagged. Last tag is $(LAST_TAG)" ; exit 1 ) grep -q "$(LAST_TAG)" CHANGELOG.md main.go || ( echo "CHANGELOG.md or main.go not updated. $(LAST_TAG) not found"; exit 1 ) # tag tags the build .PHONY: tag tag: build/tag.sh # gorelease runs goreleaser to build and publish the artifacts .PHONY: gorelease .RELEASE.CHANGELOG.md gorelease: changelog [ -x "$(GORELEASER)" ] || ( echo "goreleaser not installed"; exit 1) GOVERSION=$(GOVERSION) goreleaser --rm-dist --release-notes=.RELEASE.CHANGELOG.md .PHONY: goreleasedryrun goreleasedryrun: changelog [ -x "$(GORELEASER)" ] || ( echo "goreleaser not installed"; exit 1) GOVERSION=$(GOVERSION) goreleaser --rm-dist --skip-publish --skip-validate --release-notes=.RELEASE.CHANGELOG.md .PHONY: changelog changelog: RELEASE=$(CUR_TAG) build/releasenotes.pl .RELEASE.CHANGELOG.md # homebrew updates the brew recipe since goreleaser can only # handle taps right now. .PHONY: homebrew homebrew: build/homebrew.sh $(LAST_TAG) # docker-test runs make test in a Docker container with # pinned versions of the external dependencies # # We download the binaries outside the Docker build to # cache the binaries and prevent repeated downloads since # ADD downloads the file every time. .PHONY: docker-test docker-test: docker build \ --build-arg consul_version=$(CI_CONSUL_VERSION) \ --build-arg vault_version=$(CI_VAULT_VERSION) \ -t test-fabio \ -f Dockerfile \ . # travis runs tests on Travis CI .PHONY: travis travis: wget -q -O ~/consul.zip https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip wget -q -O ~/vault.zip https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip unzip -o -d ~/bin ~/consul.zip unzip -o -d ~/bin ~/vault.zip vault --version consul --version make test # travis-pages runs the GitHub pages (https://fabiolb.net/) deploy on Travis CI .PHONY: travis-pages travis-pages: wget -q -O ~/hugo.tgz https://github.com/gohugoio/hugo/releases/download/v$(CI_HUGO_VERSION)/hugo_$(CI_HUGO_VERSION)_Linux-64bit.tar.gz tar -C ~/bin -zxf ~/hugo.tgz hugo hugo version (cd docs && hugo --verbose) # github runs tests on github actions .PHONY: github github: wget -q -O ~/consul.zip https://releases.hashicorp.com/consul/$(CI_CONSUL_VERSION)/consul_$(CI_CONSUL_VERSION)_linux_amd64.zip wget -q -O ~/vault.zip https://releases.hashicorp.com/vault/$(CI_VAULT_VERSION)/vault_$(CI_VAULT_VERSION)_linux_amd64.zip wget -q -O ~/gobgp.tar.gz https://github.com/osrg/gobgp/releases/download/v$(CI_GOBGP_VERSION)/gobgp_$(CI_GOBGP_VERSION)_linux_amd64.tar.gz unzip -o -d ~/bin ~/consul.zip unzip -o -d ~/bin ~/vault.zip tar xzf ~/gobgp.tar.gz -C ~/bin vault --version consul --version make test # github-pages runs the GitHub pages (https://fabiolb.net/) deploy on github actions .PHONY: github-pages github-pages: wget -q -O ~/hugo.tgz https://github.com/gohugoio/hugo/releases/download/v$(CI_HUGO_VERSION)/hugo_$(CI_HUGO_VERSION)_Linux-64bit.tar.gz mkdir -p ~/bin tar -C ~/bin -zxf ~/hugo.tgz hugo hugo version (cd docs && hugo) # clean-adm cleans up all downloaded assets in admin/ui .PHONY: clean-adm clean-adm: rm -rf admin/ui/assets/cdnjs.cloudflare.com/ajax/libs/materialize rm -f admin/ui/assets/code.jquery.com/*.js rm -f admin/ui/assets/fonts/material* rm -f admin/ui/assets/fonts/Material* # clean removes intermediate files .PHONY: clean clean: go clean rm -rf pkg dist fabio find . -name '*.test' -delete .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 # clean-all executes all "clean*" commands .PHONY: clean-all clean-all: clean clean-adm ================================================ FILE: NOTICES.txt ================================================ fabio https://github.com/fabiolb/fabio License: MIT (https://github.com/fabiolb/fabio/LICENSE) Copyright (c) 2017 Frank Schroeder. All rights reserved. (after 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870) Copyright (c) 2015 eBay Software Foundation. All rights reserved. (before 15 Apr 2017/commit 38f73da6413b68fed1631101ac1d0b79a2fac870) ------------------------------------------------ Attribution for Project Dependencies ------------------------------------------------ github.com/armon/go-proxyproto https://github.com/armon/go-proxyproto License: MIT (https://github.com/armon/go-proxyproto/LICENSE) Copyright (c) 2014 Armon Dadgar github.com/circonus-labs/circonus-gometrics https://github.com/circonus-labs/circonus-gometrics License: BSD 3-clause (https://github.com/circonus-labs/circonus-gometrics/LICENSE) Copyright (c) 2016, Circonus, Inc. All rights reserved. github.com/circonus-labs/circonusllhist https://github.com/circonus-labs/circonusllhist License: BSD 3-clause (https://github.com/circonus-labs/circonusllhist/LICENSE) Copyright (c) 2016 Circonus, Inc. All rights reserved. github.com/cyberdelia/go-metrics-graphite https://github.com/cyberdelia/go-metrics-graphite License: BSD 2-clause (https://github.com/cyberdelia/go-metrics-graphite/LICENSE) Copyright 2015 Timothée Peignier. All rights reserved. github.com/fatih/structs https://github.com/fatih/structs License: MIT (https://github.com/fatih/structs/LICENSE) Copyright (c) 2014 Fatih Arslan github.com/hashicorp/consul https://github.com/hashicorp/consul License: MPL-2 (https://github.com/hashicorp/consul/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/errwrap https://github.com/hashicorp/errwrap License: MPL-2 (https://github.com/hashicorp/errwrap/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/go-cleanhttp https://github.com/hashicorp/go-cleanhttp License: MPL-2 (https://github.com/hashicorp/go-cleanhttp/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/go-multierror https://github.com/hashicorp/go-multierror License: MPL-2 (https://github.com/hashicorp/go-multierror/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/go-retryablehttp https://github.com/hashicorp/go-retryablehttp License: MPL-2 (https://github.com/hashicorp/go-retryablehttp/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/go-rootcerts https://github.com/hashicorp/go-rootcerts License: MPL-2 (https://github.com/hashicorp/go-rootcerts/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/hcl https://github.com/hashicorp/hcl License: MPL-2 (https://github.com/hashicorp/hcl/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/serf https://github.com/hashicorp/serf License: MPL-2 (https://github.com/hashicorp/serf/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/hashicorp/vault https://github.com/hashicorp/vault License: MPL-2 (https://github.com/hashicorp/vault/LICENSE) Copyright 2017 HashiCorp, Inc. github.com/pubnub/go-metrics-statsd https://github.com/pubnub/go-metrics-statsd License: MIT (https://github.com/pubnub/go-metrics-statsd/LICENSE) Copyright (c) 2016 PubNub github.com/magiconair/properties https://github.com/magiconair/properties License: BSD 2-clause (https://github.com/magiconair/properties/LICENSE) Copyright (c) 2013-2017 - Frank Schroeder github.com/mitchellh/go-homedir https://github.com/mitchellh/go-homedir License: MIT (https://github.com/mitchellh/go-homedir/LICENSE) Copyright (c) 2013 Mitchell Hashimoto github.com/mitchellh/mapstructure https://github.com/mitchellh/mapstructure License: MIT (https://github.com/mitchellh/mapstructure/LICENSE) Copyright (c) 2013 Mitchell Hashimoto github.com/rcrowley/go-metrics https://github.com/rcrowley/go-metrics License: BSD 2-clause (https://github.com/rcrowley/go-metrics/LICENSE) Copyright 2012 Richard Crowley. All rights reserved. github.com/ryanuber/go-glob https://github.com/ryanuber/go-glob License: MIT (https://github.com/ryanuber/go-glob/LICENSE) Copyright (c) 2014 Ryan Uber github.com/sergi/go-diff https://github.com/sergi/go-diff License: MIT (https://github.com/sergi/go-diff/LICENSE) Copyright (c) 2012-2016 The go-diff Authors. All rights reserved. github.com/rakyll/statik https://github.com/rakyll/statik License: Apache-2.0 (https://github.com/rakyll/statik/LICENSE) Copyright (c) 2014 Google Inc. All Rights Reserved. MaterializeCSS https://materializecss.com/ License: MIT (https://github.com/dogfalo/materialize/LICENSE) Copyright (c) 2018 Materialize jQuery https://jquery.com/ License: MIT (https://github.com/jquery/jquery/LICENSE) Copyright (c) JS Foundation and other contributors, https://js.foundation/ Material Design icons http://google.github.io/material-design-icons/ License: Apache-2.0 (https://github.com/google/material-design-icons/LICENSE) Copyright 2015 Google, Inc. All Rights Reserved. golang.org/x/net https://golang.org/x/net License: BSD 3-clause (https://golang.org/x/net/LICENSE) Copyright (c) 2009 The Go Authors. All rights reserved. golang.org/go https://github.com/golang/go License: BSD 3-clause (https://github.com/golang/go/LICENSE) Copyright (c) 2009 The Go Authors. All rights reserved. github.com/rogpeppe/fastuuid https://github.com/rogpeppe/fastuuid.git License: BSD 3-clause (https://github.com/google/uuid/LICENSE) Copyright © 2014, Roger Peppe All rights reserved. golang.org/x/sync/singleflight https://golang.org/x/sync/singleflight License: BSD 3-clause (https://golang.org/x/sync/LICENSE) Copyright (c) 2009 The Go Authors. All rights reserved. ================================================ FILE: README.md ================================================

Release License MIT Github Actions Build Status Downloads Docker Pulls fabiolb

--- #### Notes 1) From release 1.6.1 onward, the minimum golang version supported is 1.16. 2) From release 1.6.0 onward, metrics backend statsd is no longer supported. statsd_raw works similarly, though it actually resets counters appropriately. If you are using datadog, you should consider using the new dogstatsd backend, which has support for tags now. Graphite histogram functionality has changed slightly since switching to gokit framework, so something to be aware of. Prometheus functionality is now supported natively. 3) From release 1.5.15 onward, fabio changes the default GOGC from 800 back to the golang default of 100. Apparently this made some sense back in the golang 1.5 days, but with changes introduced with golang 1.12 and others, this is probably no longer a very good default. This is still configurable, as always, but the new default should make the most sense for most users. 4) From release 1.5.14, release hashes are signed with a new PGP key. See details [here](https://fabiolb.net/faq/verifying-releases/). 5) From release 1.5.14 onward, fabio binary releases are compiled with golang 1.15+. This means that the fabio will no longer validate upstream https certificates that do not have SAN extensions matching the server name. This may be a concern if fabio is communicating with https backends with misconfigured certificates. If this is a problem, you can specify `tlsskipverify=true` on the route. --- fabio is a fast, modern, zero-conf load balancing HTTP(S) and TCP router for deploying applications managed by [consul](https://consul.io/). Register your services in consul, provide a health check and fabio will start routing traffic to them. No configuration required. Deployment, upgrading and refactoring has never been easier. fabio is developed and maintained by The Fabio Authors. It powers some of the largest websites in Australia ([gumtree.com.au](http://www.gumtree.com.au)). It delivers 23.000 req/sec every day since Sep 2015 without problems. It integrates with [Consul](https://consul.io/), [Vault](https://vaultproject.io/), [Amazon ELB](https://aws.amazon.com/elasticloadbalancing), [Amazon API Gateway](https://aws.amazon.com/api-gateway/) and more. It supports ([Full feature list](https://fabiolb.net/feature/)) * [TLS termination with dynamic certificate stores](https://fabiolb.net/feature/certificate-stores/) * [Raw TCP proxy](https://fabiolb.net/feature/tcp-proxy/) * [TCP+SNI proxy for full end-to-end TLS](https://fabiolb.net/feature/tcp-sni-proxy/) without decryption * [HTTPS+TCP+SNI proxy for TCP+SNI with HTTPS fallback](https://fabiolb.net/feature/https-tcp-sni-proxy/) * [TCP dynamic proxy](https://fabiolb.net/feature/tcp-dynamic-proxy/) * [HTTPS upstream support](https://fabiolb.net/feature/https-upstream/) * [Websockets](https://fabiolb.net/feature/websockets/) and * [SSE](https://fabiolb.net/feature/sse/) * [Dynamic reloading without restart](https://fabiolb.net/feature/dynamic-reloading/) * [Traffic shaping](https://fabiolb.net/feature/traffic-shaping/) for "blue/green" deployments, * [Prometheus](https://fabiolb.net/feature/metrics/), * [Circonus](https://fabiolb.net/feature/metrics/), * [Graphite](https://fabiolb.net/feature/metrics/), * [StatsD](https://fabiolb.net/feature/metrics/), * [DataDog](https://fabiolb.net/feature/metrics/) for metrics, * [WebUI](https://fabiolb.net/feature/web-ui/) and * [Advertising BGP anycast addresses](https://fabiolb.net/feature/bgp/) on non-windows platforms. [Watch](https://www.youtube.com/watch?v=gf43TcWjBrE&list=PL81sUbsFNc5b-Gd59Lpz7BW0eHJBt0GvE&index=1) Kelsey Hightower demo Consul, Nomad, Vault and fabio at HashiConf EU 2016. The full documentation is on [fabiolb.net](https://fabiolb.net/) ## Getting started 1. Install from source, [binary](https://github.com/fabiolb/fabio/releases), [Docker](https://hub.docker.com/r/fabiolb/fabio/) or [Homebrew](http://brew.sh). ```shell # go 1.15 or higher is required go install github.com/fabiolb/fabio@latest (>= go1.15) brew install fabio (OSX/macOS stable) brew install --devel fabio (OSX/macOS devel) docker pull fabiolb/fabio (Docker) https://github.com/fabiolb/fabio/releases (pre-built binaries) ``` 2. Register your service in [consul](https://consul.io/). Make sure that each instance registers with a **unique ServiceID** and a service name **without spaces**. 3. Register a **health check** in consul as described [here](https://consul.io/docs/agent/checks.html). 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/). 4. Register one `urlprefix-` tag per `host/path` prefix it serves, e.g.: ``` # HTTP/S examples urlprefix-/css # path route urlprefix-i.com/static # host specific path route urlprefix-mysite.com/ # host specific catch all route urlprefix-/foo/bar strip=/foo # path stripping (forward '/bar' to upstream) urlprefix-/foo/bar proto=https # HTTPS upstream urlprefix-/foo/bar proto=https tlsskipverify=true # HTTPS upstream and self-signed cert # TCP examples urlprefix-:3306 proto=tcp # route external port 3306 ``` Make sure the prefix for HTTP routes contains **at least one slash** (`/`). See the full list of options in the [Documentation](https://github.com/fabiolb/fabio/wiki/Routing#config-language). 5. Start fabio without a config file (assuming a running consul agent on `localhost:8500`) Watch the log output how fabio picks up the route to your service. Try starting/stopping your service to see how the routing table changes instantly. 6. Send all your HTTP traffic to fabio on port `9999`. For TCP proxying see [TCP proxy](https://fabiolb.net/feature/tcp-proxy/). 7. Done ## Author and Founder * Frank Schroeder [@magiconair](https://twitter.com/magiconair) ## Maintainers * [Education Networks of America](https://github.com/myENA/) * [Fabio Members](https://github.com/orgs/fabiolb/people) ### Contributors This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. ## License * Contributions up to 14 Apr 2017 before [38f73da](https://github.com/fabiolb/fabio/commit/38f73da6413b68fed1631101ac1d0b79a2fac870) MIT Licensed Copyright (c) 2017 eBay Software Foundation. All rights reserved. * Contributions after 14 Apr 2017 starting with [38f73da](https://github.com/fabiolb/fabio/commit/38f73da6413b68fed1631101ac1d0b79a2fac870) MIT Licensed Copyright (c) 2017-2019 Frank Schroeder. All rights reserved. * Contributions after 22 Jan 2020 starting with [9da7b1b](https://github.com/fabiolb/fabio/commit/9da7b1b6ce0f631f7974e8663b34022c3496dca7#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5) MIT Licensed Copyright (c) 2020 Education Networks of America. All rights reserved. See [LICENSE](https://github.com/fabiolb/fabio/blob/master/LICENSE) for details. ================================================ FILE: admin/api/api.go ================================================ // Package api provides the HTTP api. package api import ( "encoding/json" "log" "net/http" ) func writeJSON(w http.ResponseWriter, r *http.Request, v interface{}) { _, pretty := r.URL.Query()["pretty"] var buf []byte var err error if pretty { buf, err = json.MarshalIndent(v, "", " ") } else { buf, err = json.Marshal(v) } if err != nil { log.Print("[ERROR] ", err) http.Error(w, "internal error", 500) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write(buf) } ================================================ FILE: admin/api/config.go ================================================ package api import "net/http" type ConfigHandler struct { Config interface{} } func (h *ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { writeJSON(w, r, h.Config) } ================================================ FILE: admin/api/manual.go ================================================ package api import ( "encoding/json" "log" "net/http" "github.com/fabiolb/fabio/registry" ) // ManualHandler provides a fetch and update handler for the manual overrides api. type ManualHandler struct { BasePath string } type manual struct { Value string `json:"value"` Version uint64 `json:"version,string"` } func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // we need this for testing. // under normal circumstances this is never nil if registry.Default == nil { return } path := r.RequestURI[len(h.BasePath):] switch r.Method { case "GET": value, version, err := registry.Default.ReadManual(path) if err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, r, manual{value, version}) return case "PUT": var m manual if err := json.NewDecoder(r.Body).Decode(&m); err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusBadRequest) return } defer r.Body.Close() ok, err := registry.Default.WriteManual(path, m.Value, m.Version) if err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } if !ok { http.Error(w, "version mismatch", http.StatusConflict) return } default: http.Error(w, "not allowed", http.StatusMethodNotAllowed) } } ================================================ FILE: admin/api/paths.go ================================================ package api import ( "log" "net/http" "strings" "github.com/fabiolb/fabio/registry" ) type ManualPathsHandler struct { Prefix string } func (h *ManualPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // we need this for testing. // under normal circumstances this is never nil if registry.Default == nil { return } switch r.Method { case "GET": paths, err := registry.Default.ManualPaths() if err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } for i, p := range paths { paths[i] = strings.TrimPrefix(p, h.Prefix) } writeJSON(w, r, paths) return default: http.Error(w, "not allowed", http.StatusMethodNotAllowed) } } ================================================ FILE: admin/api/routes.go ================================================ package api import ( "fmt" "net/http" "sort" "strings" "github.com/fabiolb/fabio/route" ) type RoutesHandler struct{} type apiRoute struct { Service string `json:"service"` Host string `json:"host"` Path string `json:"path"` Src string `json:"src"` Dst string `json:"dst"` Opts string `json:"opts"` Cmd string `json:"cmd"` Tags []string `json:"tags,omitempty"` Weight float64 `json:"weight"` Rate1 float64 `json:"rate1"` Pct99 float64 `json:"pct99"` } func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := route.GetTable() if _, ok := r.URL.Query()["raw"]; ok { w.Header().Set("Content-Type", "text/plain") fmt.Fprintln(w, t.String()) return } var hosts []string for host := range t { hosts = append(hosts, host) } sort.Strings(hosts) var routes []apiRoute for _, host := range hosts { for _, tr := range t[host] { for _, tg := range tr.Targets { var opts []string for k, v := range tg.Opts { opts = append(opts, k+"="+v) } ar := apiRoute{ Service: tg.Service, Host: tr.Host, Path: tr.Path, Src: tr.Host + tr.Path, Dst: tg.URL.String(), Opts: strings.Join(opts, " "), Weight: tg.Weight, Tags: tg.Tags, Cmd: "route add", // Rate1: tg.Timer.Rate1(), // Pct99: tg.Timer.Percentile(0.99), } routes = append(routes, ar) } } } writeJSON(w, r, routes) } ================================================ FILE: admin/api/version.go ================================================ package api import ( "fmt" "net/http" ) type VersionHandler struct { Version string } func (h *VersionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s", h.Version) } ================================================ FILE: admin/server.go ================================================ package admin import ( "crypto/tls" "fmt" "net/http" "strings" "github.com/fabiolb/fabio/admin/api" "github.com/fabiolb/fabio/admin/ui" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/proxy" ) // Server provides the HTTP server for the admin UI and API. type Server struct { Cfg *config.Config Access string Color string Title string Version string Commands string } // ListenAndServe starts the admin server. func (s *Server) ListenAndServe(l config.Listen, tlscfg *tls.Config) error { return proxy.ListenAndServeHTTP(l, s.handler(), tlscfg) } func (s *Server) handler() http.Handler { mux := http.NewServeMux() switch s.Access { case "ro": mux.HandleFunc("/api/paths", forbidden) mux.HandleFunc("/api/manual", forbidden) mux.HandleFunc("/api/manual/", forbidden) mux.HandleFunc("/manual", forbidden) mux.HandleFunc("/manual/", forbidden) case "rw": // for historical reasons the configured config path starts with a '/' // but Consul treats all KV paths without a leading slash. pathsPrefix := strings.TrimPrefix(s.Cfg.Registry.Consul.KVPath, "/") mux.Handle("/api/paths", &api.ManualPathsHandler{Prefix: pathsPrefix}) mux.Handle("/api/manual", &api.ManualHandler{BasePath: "/api/manual"}) mux.Handle("/api/manual/", &api.ManualHandler{BasePath: "/api/manual"}) mux.Handle("/manual", &ui.ManualHandler{ BasePath: "/manual", Color: s.Color, Title: s.Title, Version: s.Version, Commands: s.Commands, }) mux.Handle("/manual/", &ui.ManualHandler{ BasePath: "/manual", Color: s.Color, Title: s.Title, Version: s.Version, Commands: s.Commands, }) } mux.Handle("/api/config", &api.ConfigHandler{Config: s.Cfg}) mux.Handle("/api/routes", &api.RoutesHandler{}) mux.Handle("/api/version", &api.VersionHandler{Version: s.Version}) mux.Handle("/routes", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version, RoutingTable: s.Cfg.UI.RoutingTable}) mux.HandleFunc("/health", handleHealth) mux.Handle("/assets/", http.FileServer(http.FS(ui.Static))) mux.HandleFunc("/favicon.ico", http.NotFound) mux.Handle("/", http.RedirectHandler("/routes", http.StatusSeeOther)) return mux } func handleHealth(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "OK") } func forbidden(w http.ResponseWriter, r *http.Request) { http.Error(w, "Forbidden", http.StatusForbidden) } ================================================ FILE: admin/server_test.go ================================================ package admin import ( "net/http" "net/http/httptest" "testing" "github.com/fabiolb/fabio/config" ) func TestAdminServerAccess(t *testing.T) { type test struct { uri string code int } testAccess := func(access string, tests []test) { srv := &Server{ Access: access, Cfg: &config.Config{ Registry: config.Registry{ Consul: config.Consul{ KVPath: "/fabio/config", }, }, }, } ts := httptest.NewServer(srv.handler()) defer ts.Close() noRedirectClient := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } for _, tt := range tests { t.Run(access+tt.uri, func(t *testing.T) { resp, err := noRedirectClient.Get(ts.URL + tt.uri) if err != nil { t.Fatalf("got %v want nil", err) } if got, want := resp.StatusCode, tt.code; got != want { t.Fatalf("got code %d want %d", got, want) } }) } } roTests := []test{ {"/api/manual", 403}, {"/api/paths", 403}, {"/api/config", 200}, {"/api/routes", 200}, {"/api/version", 200}, {"/manual", 403}, {"/routes", 200}, {"/health", 200}, {"/assets/logo.svg", 200}, {"/assets/logo.bw.svg", 200}, {"/", 303}, } rwTests := []test{ {"/api/manual", 200}, {"/api/paths", 200}, {"/api/config", 200}, {"/api/routes", 200}, {"/api/version", 200}, {"/manual", 200}, {"/routes", 200}, {"/health", 200}, {"/assets/logo.svg", 200}, {"/assets/logo.bw.svg", 200}, {"/", 303}, } testAccess("ro", roTests) testAccess("rw", rwTests) } ================================================ FILE: admin/ui/assets/fonts/material-icons.css ================================================ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ src: local('Material Icons'), local('MaterialIcons-Regular'), url(MaterialIcons-Regular.woff2) format('woff2'), url(MaterialIcons-Regular.woff) format('woff'), url(MaterialIcons-Regular.ttf) format('truetype'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; /* Support for Safari and Chrome. */ text-rendering: optimizeLegibility; /* Support for Firefox. */ -moz-osx-font-smoothing: grayscale; /* Support for IE. */ font-feature-settings: 'liga'; } ================================================ FILE: admin/ui/generate.go ================================================ package ui //go:generate rm -rf assets/code.jquery.com //go:generate rm -rf assets/cdnjs.cloudflare.com //go:generate wget -pP assets https://code.jquery.com/jquery-3.6.0.min.js //go:generate wget -pP assets https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js //go:generate wget -pP assets https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css // https://google.github.io/material-design-icons/#setup-method-2-self-hosting //go:generate rm -rf assets/fonts //go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.ttf //go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.eot //go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.woff //go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/MaterialIcons-Regular.woff2 //go:generate wget -nH -nd -pP assets/fonts https://raw.githubusercontent.com/google/material-design-icons/3.0.1/iconfont/material-icons.css ================================================ FILE: admin/ui/manual.go ================================================ package ui import ( "html/template" "net/http" ) type ManualHandler struct { BasePath string Color string Title string Version string Commands string } func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.RequestURI[len(h.BasePath):] data := struct { *ManualHandler Path string APIPath string }{ ManualHandler: h, Path: path, APIPath: "/api/manual" + path, } tmplManual.ExecuteTemplate(w, "manual", data) } var funcs = template.FuncMap{ "noescape": func(str string) template.HTML { return template.HTML(str) }, } var tmplManual = template.Must(template.New("manual").Funcs(funcs).Parse( // language=HTML ` fabio{{if .Title}} - {{.Title}}{{end}}
Manual Routes{{if .Path}} for "{{.Path}}"{{end}}
{{.Commands}}
`)) ================================================ FILE: admin/ui/route.go ================================================ package ui import ( "html/template" "net/http" "github.com/fabiolb/fabio/config" ) // RoutesHandler provides the UI for managing the routing table. type RoutesHandler struct { Color string Title string Version string RoutingTable config.RoutingTable } func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) { tmplRoutes.ExecuteTemplate(w, "routes", h) } var tmplRoutes = template.Must(template.New("routes").Parse( // language=HTML ` fabio{{if .Title}} - {{.Title}}{{end}}
Routing Table

`)) ================================================ FILE: admin/ui/static.go ================================================ package ui import "embed" //go:embed assets/* var Static embed.FS ================================================ FILE: assert/assert.go ================================================ // Package assert provides a simple assert framework. package assert import ( "fmt" "path/filepath" "reflect" "runtime" "testing" ) // Equal provides an assertEqual function func Equal(t *testing.T) func(got, want interface{}) { return EqualDepth(t, 1, "") } func EqualDepth(t *testing.T, calldepth int, desc string) func(got, want interface{}) { return func(got, want interface{}) { _, file, line, _ := runtime.Caller(calldepth) if !reflect.DeepEqual(got, want) { fmt.Printf("\t%s:%d: %s: got %v want %v\n", filepath.Base(file), line, desc, got, want) t.Fail() } } } ================================================ FILE: auth/auth.go ================================================ package auth import ( "fmt" "net/http" "github.com/fabiolb/fabio/config" ) type AuthScheme interface { Authorized(request *http.Request, response http.ResponseWriter) bool } func LoadAuthSchemes(cfg map[string]config.AuthScheme) (map[string]AuthScheme, error) { auths := map[string]AuthScheme{} for _, a := range cfg { switch a.Type { case "basic": b, err := newBasicAuth(a.Basic) if err != nil { return nil, err } auths[a.Name] = b default: return nil, fmt.Errorf("unknown auth type '%s'", a.Type) } } return auths, nil } ================================================ FILE: auth/auth_test.go ================================================ package auth import ( "testing" "github.com/fabiolb/fabio/config" ) func TestLoadAuthSchemes(t *testing.T) { t.Run("should fail when auth scheme fails to load", func(t *testing.T) { _, err := LoadAuthSchemes(map[string]config.AuthScheme{ "myauth": { Name: "myauth", Type: "basic", Basic: config.BasicAuth{ File: "/some/non/existent/file", }, }, }) const errorText = "open /some/non/existent/file: no such file or directory" if err.Error() != errorText { t.Fatalf("got %s, want %s", err.Error(), errorText) } }) t.Run("should return an error when auth type is unknown", func(t *testing.T) { _, err := LoadAuthSchemes(map[string]config.AuthScheme{ "myauth": { Name: "myauth", Type: "foo", }, }) const errorText = "unknown auth type 'foo'" if err.Error() != errorText { t.Fatalf("got %s, want %s", err.Error(), errorText) } }) t.Run("should load multiple auth schemes", func(t *testing.T) { myauth, err := createBasicAuthFile("foo:bar", t) if err != nil { t.Fatalf("could not create file on disk %s", err) } myotherauth, err := createBasicAuthFile("bar:foo", t) if err != nil { t.Fatalf("could not create file on disk %s", err) } result, _ := LoadAuthSchemes(map[string]config.AuthScheme{ "myauth": { Name: "myauth", Type: "basic", Basic: config.BasicAuth{ File: myauth, }, }, "myotherauth": { Name: "myotherauth", Type: "basic", Basic: config.BasicAuth{ File: myotherauth, }, }, }) if len(result) != 2 { t.Fatalf("expected 2 auth schemes, got %d", len(result)) } }) } ================================================ FILE: auth/basic.go ================================================ package auth import ( "bytes" "log" "net/http" "os" "time" "github.com/fabiolb/fabio/config" "github.com/tg123/go-htpasswd" ) // basic is an implementation of AuthScheme type basic struct { secrets *htpasswd.File realm string } func newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) { bad := func(err error) { log.Println("[WARN] Error processing a line in an htpasswd file:", err) } secrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, bad) if err != nil { return nil, err } if cfg.Refresh > 0 { stat, err := os.Stat(cfg.File) if err != nil { return nil, err } cfg.ModTime = stat.ModTime() go func() { cleared := false ticker := time.NewTicker(cfg.Refresh).C for range ticker { stat, err := os.Stat(cfg.File) if err != nil { log.Println("[WARN] Error accessing htpasswd file:", err) if !cleared { err = secrets.ReloadFromReader(&bytes.Buffer{}, bad) if err != nil { log.Println("[WARN] Error clearing the htpasswd credentials:", err) } else { log.Println("[INFO] The htpasswd credentials have been cleared") cleared = true } } continue } // refresh the htpasswd file only if its modification time has changed // even if the new htpasswd file is older than previously loaded if cfg.ModTime != stat.ModTime() { if err := secrets.Reload(bad); err == nil { log.Println("[INFO] The htpasswd file has been successfully reloaded") cfg.ModTime = stat.ModTime() cleared = false } else { log.Println("[WARN] Error reloading htpasswd file:", err) } } } }() } return &basic{ secrets: secrets, realm: cfg.Realm, }, nil } func (b *basic) Authorized(request *http.Request, response http.ResponseWriter) bool { user, password, ok := request.BasicAuth() if !ok { response.Header().Set("WWW-Authenticate", "Basic realm=\""+b.realm+"\"") return false } return b.secrets.Match(user, password) } ================================================ FILE: auth/basic_test.go ================================================ package auth import ( "encoding/base64" "fmt" "net/http" "os" "reflect" "strings" "testing" "time" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/uuid" ) type responseWriter struct { header http.Header code int written []byte } func (rw *responseWriter) Header() http.Header { if rw.header == nil { rw.header = map[string][]string{} } return rw.header } func (rw *responseWriter) Write(b []byte) (int, error) { rw.written = append(rw.written, b...) return len(rw.written), nil } func (rw *responseWriter) WriteHeader(statusCode int) { rw.code = statusCode } func createBasicAuthFile(contents string, t *testing.T) (string, error) { dir := t.TempDir() filename := fmt.Sprintf("%s/%s", dir, uuid.NewUUID()) err := os.WriteFile(filename, []byte(contents), 0666) if err != nil { return "", fmt.Errorf("could not write password file: %s", err) } return filename, nil } func createBasicAuth(user string, password string, t *testing.T) (AuthScheme, error) { contents := fmt.Sprintf("%s:%s", user, password) filename, err := createBasicAuthFile(contents, t) if err != nil { return nil, fmt.Errorf("could not create basic auth: %s", err) } a, err := newBasicAuth(config.BasicAuth{ File: filename, Realm: "testrealm", }) if err != nil { return nil, fmt.Errorf("could not create basic auth: %s", err) } return a, nil } func TestNewBasicAuth(t *testing.T) { t.Run("should create a basic auth scheme from the supplied config", func(t *testing.T) { filename, err := createBasicAuthFile("foo:bar", t) if err != nil { t.Error(err) } _, err = newBasicAuth(config.BasicAuth{ File: filename, }) if err != nil { t.Error(err) } }) t.Run("should log a warning when credentials are malformed", func(t *testing.T) { filename, err := createBasicAuthFile("foosdlijdgohdgdbar", t) if err != nil { t.Error(err) } _, err = newBasicAuth(config.BasicAuth{ File: filename, }) if err != nil { t.Error(err) } }) } func TestBasic_Authorised(t *testing.T) { basicAuth, err := createBasicAuth("foo", "bar", t) creds := []byte("foo:bar") if err != nil { t.Fatal(err) } tests := []struct { name string req *http.Request res http.ResponseWriter out bool }{ { "correct credentials should be authorized", &http.Request{ Header: http.Header{ "Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(creds))}, }, }, &responseWriter{}, true, }, { "incorrect credentials should not be authorized", &http.Request{ Header: http.Header{ "Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("baz:blarg")))}, }, }, &responseWriter{}, false, }, { "missing Authorization header should not be authorized", &http.Request{ Header: http.Header{}, }, &responseWriter{}, false, }, { "malformed Authorization header should not be authorized", &http.Request{ Header: http.Header{ "Authorization": []string{"malformed"}, }, }, &responseWriter{}, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got, want := basicAuth.Authorized(tt.req, tt.res), tt.out; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } } func TestBasic_Authorised_should_fail_without_htpasswd_file(t *testing.T) { filename, err := createBasicAuthFile("foo:bar", t) if err != nil { t.Error(err) } a, err := newBasicAuth(config.BasicAuth{ File: filename, Refresh: time.Second, }) if err != nil { t.Error(err) } creds := []byte("foo:bar") r := &http.Request{ Header: http.Header{ "Authorization": []string{fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(creds))}, }, } w := &responseWriter{} t.Run("should authorize against supplied htpasswd file", func(t *testing.T) { if got, want := a.Authorized(r, w), true; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) if err := os.Remove(filename); err != nil { t.Fatalf("removing htpasswd file: %s", err) } time.Sleep(2 * time.Second) // ensure htpasswd file refresh happened t.Run("should not authorize after removing htpasswd file", func(t *testing.T) { if got, want := a.Authorized(r, w), false; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } func TestBasic_Authorized_should_set_www_realm_header(t *testing.T) { basicAuth, err := createBasicAuth("foo", "bar", t) if err != nil { t.Fatal(err) } rw := &responseWriter{} _ = basicAuth.Authorized(&http.Request{Header: http.Header{}}, rw) got := rw.Header().Get("WWW-Authenticate") want := `Basic realm="testrealm"` if strings.Compare(got, want) != 0 { t.Errorf("got '%s', want '%s'", got, want) } } ================================================ FILE: bgp/bgp_nonwindows.go ================================================ //go:build !windows // +build !windows package bgp import ( "context" "errors" "fmt" "log" "net" "os" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/exit" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/proto" apb "google.golang.org/protobuf/types/known/anypb" api "github.com/osrg/gobgp/v3/api" bgpconfig "github.com/osrg/gobgp/v3/pkg/config" "github.com/osrg/gobgp/v3/pkg/server" ) var ( ErrMissingAnycast = errors.New("you must specify at least one anycast address to advertise") ErrMissingPeers = errors.New("you must specify at least one peer to advertise routes to") ErrMissingRouterID = errors.New("you must specify the routerID of this host, i.e. a non anycast address") ErrNoRoutesAdded = errors.New("no routes were successfully added") ErrNoRoutesDeleted = errors.New("no routes were successfully deleted") ErrNoPeersAdded = errors.New("no peers were successfully added") ) const ( denyAllNeighbors = "deny-all-neighbors" matchAnyPeer = "match-any-peer" globalTable = "global" rejectAll = "reject-all" ) type BGPHandler struct { server *server.BgpServer config *config.BGP routeAttrs []*apb.Any } func NewBGPHandler(config *config.BGP) (*BGPHandler, error) { // pre-chew some protobuf messages that are part of // every anycast route we'll be adding. nextHop := config.RouterID if len(config.NextHop) > 0 { nextHop = config.NextHop } var messages = []proto.Message{ &api.OriginAttribute{ Origin: 0, }, &api.NextHopAttribute{ NextHop: nextHop, }, &api.AsPathAttribute{ Segments: []*api.AsSegment{ { Type: api.AsSegment_AS_SEQUENCE, Numbers: []uint32{uint32(config.Asn)}, }, }, }, } attributes := make([]*apb.Any, 0, len(messages)) for _, p := range messages { attr, err := apb.New(p) if err != nil { // should never happen panic(err) } attributes = append(attributes, attr) } var opts = []server.ServerOption{server.LoggerOption(bgpLogger{})} if config.EnableGRPC { maxSize := 256 << 20 grpcOpts := []grpc.ServerOption{grpc.MaxRecvMsgSize(maxSize), grpc.MaxSendMsgSize(maxSize)} if config.GRPCTLS { creds, err := credentials.NewServerTLSFromFile(config.CertFile, config.KeyFile) if err != nil { // shouldn't get here if validate was called first. return nil, fmt.Errorf("error parsing bgp TLS credentials: %s", err) } grpcOpts = append(grpcOpts, grpc.Creds(creds)) } opts = append(opts, server.GrpcOption(grpcOpts), server.GrpcListenAddress(config.GRPCListenAddress), ) } return &BGPHandler{ server: server.NewBgpServer(opts...), config: config, routeAttrs: attributes, }, nil } func (bgph *BGPHandler) Start() error { s := bgph.server go s.Serve() if len(bgph.config.GOBGPDCfgFile) > 0 { initialCfg, err := bgpconfig.ReadConfigFile(bgph.config.GOBGPDCfgFile, "toml") if err != nil { // shouldn't happen if we called validate first. return err } _, err = bgpconfig.InitialConfig(context.Background(), s, initialCfg, false) if err != nil { return fmt.Errorf("bgp: error initializing from gobgp config: %w", err) } } else { // If we weren't passed a gobgp config file, configure using the values passed from the fabio // config, and make sure we have a sane policy where we export our routes to peers but don't // import from any peers. err := bgph.startBGP(context.Background()) if err != nil { return fmt.Errorf("bgp: error starting: %w", err) } err = bgph.setPolicies() if err != nil { return fmt.Errorf("bgp error setting policy: %w", err) } } errCh := make(chan error, 1) exit.Listen(func(sig os.Signal) { log.Printf("[INFO] Stopping BGP") err := s.StopBgp(context.Background(), &api.StopBgpRequest{}) errCh <- err }) // monitor the change of the peer state if err := s.WatchEvent(context.Background(), &api.WatchEventRequest{Peer: &api.WatchEventRequest_Peer{}}, func(r *api.WatchEventResponse) { if p := r.GetPeer(); p != nil && p.Type == api.WatchEventResponse_PeerEvent_STATE { log.Printf("[DEBUG] bgp event: %#v", p) } }); err != nil { log.Printf("[ERROR] bgp watcher failed: %s", err) } if len(bgph.config.GOBGPDCfgFile) == 0 || len(bgph.config.Peers) > 0 { // add peers err := bgph.addNeighbors(context.Background(), bgph.config.Peers) if err != nil { return fmt.Errorf("bgp error adding neighbors: %w", err) } } if len(bgph.config.AnycastAddresses) > 0 { err := bgph.AddRoutes(context.Background(), bgph.config.AnycastAddresses) if err != nil { return fmt.Errorf("bgp error adding anycastaddresses: %w", err) } } // hang until exit handler completes above. return <-errCh } func (bgph *BGPHandler) startBGP(ctx context.Context) error { return bgph.server.StartBgp(ctx, &api.StartBgpRequest{ Global: &api.Global{ Asn: uint32(bgph.config.Asn), RouterId: bgph.config.RouterID, ListenPort: int32(bgph.config.ListenPort), ListenAddresses: bgph.config.ListenAddresses, }, }) } func (bgph *BGPHandler) setPolicies() error { // Create a policy that denies all routes from any neighbor. err := bgph.server.SetPolicies(context.Background(), &api.SetPoliciesRequest{ DefinedSets: []*api.DefinedSet{ { DefinedType: api.DefinedType_NEIGHBOR, Name: matchAnyPeer, List: []string{"0.0.0.0/0", "::/0"}, }, }, Policies: []*api.Policy{ { Name: denyAllNeighbors, Statements: []*api.Statement{ { Name: rejectAll, Conditions: &api.Conditions{ NeighborSet: &api.MatchSet{ Name: matchAnyPeer, }, }, Actions: &api.Actions{ RouteAction: api.RouteAction_REJECT, }, }, }, }, }, }) if err != nil { return err } // Assign the above to the global policy return bgph.server.SetPolicyAssignment(context.Background(), &api.SetPolicyAssignmentRequest{ Assignment: &api.PolicyAssignment{ Name: globalTable, // this is the global rib Direction: api.PolicyDirection_IMPORT, Policies: []*api.Policy{ { Name: denyAllNeighbors, }, }, // Need to set default action to accept here because otherwise // even routes added via API calls get rejected. DefaultAction: api.RouteAction_ACCEPT, }, }) } func (bgph *BGPHandler) addNeighbors(ctx context.Context, peers []config.BGPPeer) error { var errs []error peerCount := 0 for _, peer := range peers { var hop *api.EbgpMultihop if peer.MultiHop { hop = &api.EbgpMultihop{ Enabled: true, MultihopTtl: uint32(peer.MultiHopLength), } } var trans *api.Transport if peer.NeighborPort > 0 { trans = &api.Transport{ LocalAddress: bgph.config.RouterID, MtuDiscovery: false, PassiveMode: false, RemoteAddress: peer.NeighborAddress, RemotePort: uint32(peer.NeighborPort), TcpMss: 0, BindInterface: "", } } err := bgph.server.AddPeer(ctx, &api.AddPeerRequest{ Peer: &api.Peer{ Conf: &api.PeerConf{ AuthPassword: peer.Password, NeighborAddress: peer.NeighborAddress, PeerAsn: uint32(peer.Asn), }, EbgpMultihop: hop, Transport: trans, }, }) if err != nil { errs = append(errs, err) continue } peerCount++ } if peerCount == 0 { errs = append(errs, ErrNoPeersAdded) } return errors.Join(errs...) } func (bgph *BGPHandler) AddRoutes(ctx context.Context, routes []string) error { var errs []error // Add our Anycast routes routesAdded := 0 for _, addr := range routes { _, ipnet, err := net.ParseCIDR(addr) if err != nil { errs = append(errs, err) continue } prefixLen, _ := ipnet.Mask.Size() af := api.Family_AFI_IP if ipnet.IP.To4() == nil { af = api.Family_AFI_IP6 } nlri, _ := apb.New(&api.IPAddressPrefix{ PrefixLen: uint32(prefixLen), Prefix: ipnet.IP.String(), }) _, err = bgph.server.AddPath(ctx, &api.AddPathRequest{ Path: &api.Path{ Nlri: nlri, Pattrs: bgph.routeAttrs, Family: &api.Family{ Afi: af, Safi: api.Family_SAFI_UNICAST, }, }, }) if err != nil { log.Printf("[ERROR] bgp error adding path for %s: %s", addr, err) errs = append(errs, fmt.Errorf("error adding %s: %w", addr, err)) } else { log.Printf("[INFO] bgp successfully added path for %s", addr) routesAdded++ } } if routesAdded == 0 { errs = append(errs, ErrNoRoutesAdded) } return errors.Join(errs...) } func (bgph *BGPHandler) DeleteRoutes(ctx context.Context, routes []string) error { var errs []error delCount := 0 for _, addr := range routes { _, ipnet, err := net.ParseCIDR(addr) if err != nil { errs = append(errs, err) continue } prefixLen, _ := ipnet.Mask.Size() af := api.Family_AFI_IP if ipnet.IP.To4() == nil { af = api.Family_AFI_IP6 } nlri, _ := apb.New(&api.IPAddressPrefix{ PrefixLen: uint32(prefixLen), Prefix: ipnet.IP.String(), }) err = bgph.server.DeletePath(ctx, &api.DeletePathRequest{ TableType: api.TableType_GLOBAL, Path: &api.Path{ Nlri: nlri, Family: &api.Family{ Afi: af, Safi: api.Family_SAFI_UNICAST, }, Pattrs: bgph.routeAttrs, }, }) if err != nil { errs = append(errs, err) continue } delCount++ } if delCount == 0 { errs = append(errs, ErrNoRoutesDeleted) } return errors.Join(errs...) } func ValidateConfig(config *config.BGP) error { if !config.BGPEnabled { return nil } for _, addr := range config.AnycastAddresses { _, _, err := net.ParseCIDR(addr) if err != nil { return fmt.Errorf("could not parse cidr for anycast address %s: %w", addr, err) } } if config.EnableGRPC && config.GRPCTLS { _, err := credentials.NewServerTLSFromFile(config.CertFile, config.KeyFile) if err != nil { return fmt.Errorf("could not parse bgp tls credentials: %w", err) } } for _, peer := range config.Peers { if net.ParseIP(peer.NeighborAddress) == nil { return fmt.Errorf("peer address %s is not a valid IP", peer.NeighborAddress) } } if len(config.GOBGPDCfgFile) > 0 { _, err := bgpconfig.ReadConfigFile(config.GOBGPDCfgFile, "toml") if err != nil { return fmt.Errorf("could not open %s: %w", config.GOBGPDCfgFile, err) } // otherwise we skip the rest of these checks, hopefully the provided bobgpd config is sane. return nil } if len(config.AnycastAddresses) == 0 { return ErrMissingAnycast } if len(config.Peers) == 0 { return ErrMissingPeers } if len(config.RouterID) == 0 { return ErrMissingRouterID } if net.ParseIP(config.RouterID) == nil { return fmt.Errorf("router ID %s is not a valid ID", config.RouterID) } if len(config.NextHop) > 0 { if ip := net.ParseIP(config.NextHop); ip == nil { return fmt.Errorf("invalid NextHop: %s", config.NextHop) } } return nil } ================================================ FILE: bgp/bgp_nonwindows_test.go ================================================ //go:build !windows // +build !windows package bgp import ( "context" "encoding/json" "github.com/fabiolb/fabio/config" api "github.com/osrg/gobgp/v3/api" "os" "os/exec" "path/filepath" "testing" "time" ) func TestBGPHandler(t *testing.T) { serverCmd := &gobgpserver{ cmdPath: "gobgpd", } err := serverCmd.start() if err != nil { t.Logf("error calling gobgpd command, probably not installed. skipping: %s", err) t.SkipNow() } defer serverCmd.stop() cfg := &config.BGP{ BGPEnabled: true, Asn: 65000, AnycastAddresses: []string{"1.2.3.4/32"}, RouterID: "127.0.0.2", ListenPort: 1790, ListenAddresses: []string{"127.0.0.2"}, Peers: []config.BGPPeer{ { NeighborAddress: "127.0.0.3", NeighborPort: 1790, Asn: 65001, MultiHop: false, }, }, EnableGRPC: true, GRPCListenAddress: "127.0.0.2:50051", NextHop: "1.2.3.4", } bh, err := NewBGPHandler(cfg) if err != nil { t.Fatal(err) } go bh.server.Serve() defer bh.server.Stop() err = bh.startBGP(context.Background()) if err != nil { t.Fatalf("error starting BGP: %s", err) } err = bh.addNeighbors(context.Background(), cfg.Peers) if err != nil { t.Fatalf("error adding neighbors: %s", err) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) defer cancel() if err := bh.server.WatchEvent(context.Background(), &api.WatchEventRequest{Peer: &api.WatchEventRequest_Peer{}}, func(r *api.WatchEventResponse) { if p := r.GetPeer(); p != nil && p.Type == api.WatchEventResponse_PeerEvent_STATE { t.Logf("EVENT RECEIVED %#v", p.Peer) if p.Peer.State.SessionState == api.PeerState_ESTABLISHED { cancel() } } }); err != nil { t.Fatal(err) } <-ctx.Done() if ctx.Err() == context.DeadlineExceeded { t.Fatal("context deadline exceeded") } gc := gobpgclient{ cmdPath: "gobgp", hostAddr: "127.0.0.3", } // now start a test table for _, tst := range []struct { name string cmd func() error routeKeys []string }{ { name: "test add route", cmd: func() error { return bh.AddRoutes(context.Background(), cfg.AnycastAddresses) }, routeKeys: []string{"1.2.3.4/32"}, }, { name: "test delete route", cmd: func() error { return bh.DeleteRoutes(context.Background(), []string{"1.2.3.4/32"}) }, routeKeys: nil, }, } { t.Run(tst.name, func(t *testing.T) { err := tst.cmd() if err != nil { t.Fatal(err) } routes, err := gc.globalRib(t) if err != nil { t.Fatal(err) } if len(routes) != len(tst.routeKeys) { t.Fatalf("routes don't match, have %d want %d", len(routes), len(tst.routeKeys)) } for _, r := range tst.routeKeys { if _, ok := routes[r]; !ok { t.Fatalf("route %s not found", r) } } }) } } type ribEntry struct { Nlri struct { Prefix string `json:"prefix"` } `json:"nlri"` Age int `json:"age"` Best bool `json:"best"` Attrs []struct { Type int `json:"type"` Value int `json:"value,omitempty"` AsPaths []struct { SegmentType int `json:"segment_type"` Num int `json:"num"` Asns []int `json:"asns"` } `json:"as_paths,omitempty"` Nexthop string `json:"nexthop,omitempty"` } `json:"attrs"` Stale bool `json:"stale"` } type gobpgclient struct { cmdPath string hostAddr string } func (gc *gobpgclient) globalRib(t *testing.T) (map[string][]ribEntry, error) { out, err := exec.Command(gc.cmdPath, "-u", gc.hostAddr, "-j", "global", "rib").Output() if err != nil { return nil, err } var rv map[string][]ribEntry err = json.Unmarshal(out, &rv) if err != nil { t.Logf("raw: %s\n", out) return nil, err } return rv, nil } type gobgpserver struct { cmdPath string cmd *exec.Cmd } func (gs *gobgpserver) start() error { gs.cmd = exec.Command(gs.cmdPath, "-p", "-f", filepath.Join("test_data", "bgp.toml"), "--api-hosts", "127.0.0.3:50051", "-l", "info") gs.cmd.Stdout = os.Stdout gs.cmd.Stderr = os.Stderr return gs.cmd.Start() } func (gs *gobgpserver) stop() error { if gs.cmd.Process != nil { return gs.cmd.Process.Kill() } return nil } ================================================ FILE: bgp/bgp_windows.go ================================================ package bgp import ( "errors" "github.com/fabiolb/fabio/config" ) type BGPHandler struct{} var ErrNoWindows = errors.New("cannot run bgp on windows") func NewBGPHandler(config *config.BGP) (*BGPHandler, error) { return nil, ErrNoWindows } func (bgph *BGPHandler) Start() error { return ErrNoWindows } func ValidateConfig(config *config.BGP) error { return ErrNoWindows } ================================================ FILE: bgp/logger.go ================================================ package bgp import ( "fmt" "log" "strings" "github.com/fabiolb/fabio/exit" "github.com/fabiolb/fabio/logger" bgplog "github.com/osrg/gobgp/v3/pkg/log" ) type bgpLogger struct{} func (l bgpLogger) Panic(msg string, fields bgplog.Fields) { exit.Fatal(convertMsgFields("FATAL", msg, fields)) } func (l bgpLogger) Fatal(msg string, fields bgplog.Fields) { exit.Fatal(convertMsgFields("FATAL", msg, fields)) } func (l bgpLogger) Error(msg string, fields bgplog.Fields) { log.Printf("%s", convertMsgFields("ERROR", msg, fields)) } func (l bgpLogger) Warn(msg string, fields bgplog.Fields) { log.Printf("%s", convertMsgFields("WARN", msg, fields)) } func (l bgpLogger) Info(msg string, fields bgplog.Fields) { log.Printf("%s", convertMsgFields("INFO", msg, fields)) } func (l bgpLogger) Debug(msg string, fields bgplog.Fields) { log.Printf("%s", convertMsgFields("DEBUG", msg, fields)) } func (l bgpLogger) SetLevel(level bgplog.LogLevel) { // noop } func (l bgpLogger) GetLevel() bgplog.LogLevel { lw, ok := log.Writer().(*logger.LevelWriter) if !ok { return bgplog.InfoLevel } switch lw.Level() { case "TRACE": return bgplog.TraceLevel case "DEBUG": return bgplog.DebugLevel case "INFO": return bgplog.InfoLevel case "WARN": return bgplog.WarnLevel case "ERROR": return bgplog.ErrorLevel case "FATAL": return bgplog.FatalLevel default: return bgplog.InfoLevel } } func convertMsgFields(level, msg string, fields bgplog.Fields) string { var b strings.Builder fmt.Fprintf(&b, "[%s] gobgpd %s", level, msg) for k, v := range fields { fmt.Fprintf(&b, " %s=>%v", k, v) } return b.String() } ================================================ FILE: bgp/test_data/bgp.toml ================================================ [[neighbors]] [neighbors.config] neighbor-address = "127.0.0.2" peer-as = 65000 [neighbors.transport.config] remote-port = 1790 passive-mode = false local-address = "127.0.0.3" [global.config] as = 65001 router-id = "127.0.0.3" port = 1790 #port = 179 local-address-list = [ "127.0.0.3" ] ================================================ FILE: build/ca-certificates.crt ================================================ -----BEGIN CERTIFICATE----- MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI 2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i 5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsx CzAJBgNVBAYTAkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRp ZmljYWNpw7NuIERpZ2l0YWwgLSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwa QUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4wHhcNMDYxMTI3MjA0NjI5WhcNMzAw NDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2Ft ZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMu QS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkq hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeG qentLhM0R7LQcNzJPNCNyu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzL fDe3fezTf3MZsGqy2IiKLUV0qPezuMDU2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQ Y5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU34ojC2I+GdV75LaeHM/J4 Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP2yYe68yQ 54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+b MMCm8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48j ilSH5L887uvDdUhfHjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++Ej YfDIJss2yKHzMI+ko6Kh3VOz3vCaMh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/zt A/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK5lw1omdMEWux+IBkAC1vImHF rEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1bczwmPS9KvqfJ pxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCB lTCBkgYEVR0gADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFy YS5jb20vZHBjLzBaBggrBgEFBQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW50 7WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2UgcHVlZGVuIGVuY29udHJhciBlbiBs YSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEfAygPU3zmpFmps4p6 xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuXEpBc unvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/ Jre7Ir5v/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dp ezy4ydV/NgIlqmjCMRW3MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42 gzmRkBDI8ck1fj+404HGIGQatlDCIaR43NAvO2STdPCWkPHv+wlaNECW8DYSwaN0 jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wkeZBWN7PGKX6jD/EpOe9+ XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f/RWmnkJD W2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/ RL5hRqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35r MDOhYil/SrnhLecUIw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxk BYn8eNZcLCZDqQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC +Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X 7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz 43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV 6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH 1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF 62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh 4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G 87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i 2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no xqE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp 6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ +jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S 5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B 8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc 0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e KeC2uAloGRwYQw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D 0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym 1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb 2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC 206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 +L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw RY8mkaKO/qk= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm /qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL rosot4LKGAfmt1t06SAZf7IbiVQ= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52 ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH 2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1 k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs 2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+ 8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE DNuxUCAKGkq6ahq97BvIxYSazQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF 6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF 661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9 am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1 ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481 PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS 3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF 3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B 5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg Q2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD VQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg isRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z NIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI +MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R hzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+ mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw AwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP Bdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s EzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2 mSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC e/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow dXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6 yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq 7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p 26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi 1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu tGWaIZDgqtCYvDi1czyL+Nw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q 130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG 9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N 8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K /OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu 7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC 28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB 0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q 619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn 2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG 5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 l7+ijrRU -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do 0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ 44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN 9u6wWk5JRFRYX0KD -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs 6GAqm4VKQPNriiTsBhYscw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9 28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR 5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s +12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5 Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1 +HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF 5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1 glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2 pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ d0jQ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO 76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj 2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe 3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI 2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp +2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW /zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB ZQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk 3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz 6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW 1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0 MRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG EwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT CkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK 8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2 98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb 2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC ejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi Xd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB o4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl ZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD AgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL AZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd foPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M cXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq 8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp hbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk Res3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U AGegcQCCSA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw PDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu MQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx GzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL MAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf HZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh gHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW v+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue Mv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr 9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt 6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7 MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl Y3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58 ADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq hkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p iL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC dsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL kz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL hfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5 7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW WL1WMRJOEcgh4LMRkWXbtKaIOM5V -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl 6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU Cm26OWMohpLzGITY+9HPBVZkVw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL EwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x ETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg bIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ j9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV Sn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI RFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw MjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5 fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i +DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG SIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN QseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+ gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL EwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x ETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/ k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso LeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o TQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3 MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C TShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5 WzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG SIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR xdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4 MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h 4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4 c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z +kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2 l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB /wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+ 8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI 6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4 7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7 QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1 MQswCQYDVQQGEwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxp Z2kgQS5TLjE8MDoGA1UEAxMzZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZp a2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3MDEwNDExMzI0OFoXDTE3MDEwNDEx MzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0cm9uaWsgQmlsZ2kg R3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9uaWsg U2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdU MZTe1RK6UxYC6lhj71vY8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlT L/jDj/6z/P2douNffb7tC+Bg62nsM+3YjfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H 5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAIJjjcJRFHLfO6IxClv7wC 90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk9Ok0oSy1 c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/ BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoE VtstxNulMA0GCSqGSIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLP qk/CaOv/gKlR6D1id4k9CnU58W5dF4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S /wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwqD2fK/A+JYZ1lpTzlvBNbCNvj /+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4Vwpm+Vganf2X KWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH 4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er fF6adulZkMV8gzURZVE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi 94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS /jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D hNQ+IIX3Sj0rnP0qCglN6oH4EZw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y 7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh 1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN /Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc 58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv 8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL 5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe 2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv /NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz 4iIprn2DQKi6bA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl 4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2 MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz rD6ogRLQy7rQkgu2npaqBA+K -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz +uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn 5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G spki4cErx5z481+oghLrGREt -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m 1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH 6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB /wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG 9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7 G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2 zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4 ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2 Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r 6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z 09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG 3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO 291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH WD9f -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h /t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf ReYNnyicsbkqWletNw+vHX/bvZ8= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH /PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu 9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 4uJEvlz36hz1 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9 nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3 7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi AmvZWg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG 9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R 0982gaEbeC9xs/FZTEYYKKuF0mBWWg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL 2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z TbvGRNs2yyqcjg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 +rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c 2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx 62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS 8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl 7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a 86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C +C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD EylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05 OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l dExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG SIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc Q7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E BAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G SUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu b3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv Y2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln aXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0 IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph biBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo ZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP UlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj YXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA bmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06 sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS NitjrFgBazMpUIaD8QFI -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD EytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X DTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw DwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u c2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr TmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC 2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW RMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P AQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW ggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0 YWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz b2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO ZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB IGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs b2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s YXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg a2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g SU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0 aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg YXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg Y3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4 Fp1hBWeAyNDYpQcCNJgEjTME1A== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi 3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP 0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK 8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5 3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0 TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6 b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0 ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3 dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3 Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+ NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY 83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3 macqaJVmlaut74nLYKkGEsaUR+ko -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4 qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8 6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/ h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH /nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg 4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ /L7fCg0= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp +ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og /zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB 4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd 8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A 4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd +LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B 4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK 4SVhM7JZG+Ju1zdXtg2pEto= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0 aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0 aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8 7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK SnQ2+Q== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg 9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg /9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch 6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 7CAFYd4= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa /FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0 MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8 h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9 UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni 8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5 pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN QSdJQO7e5iNEOdyhIta6A/I= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO 0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj 7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS 8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ 3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR 3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG 29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk 3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt 5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s 3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu 8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/ 3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6 Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA 7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k /rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy 7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp 5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy 5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv 6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen 5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL +63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf 8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN +lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA 1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg 8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk 6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn 0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN sSi6 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w +2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B 26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ 9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB rjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp MRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz c2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u IGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa Fw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t V3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg RGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV U1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1 toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo TUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy ggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1 XgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF hy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm 7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG MCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV HQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp ttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo LtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF iXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y h9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I k63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo 19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e 3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 MBr1mmz0DlP5OlvRHA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c 6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn 8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a 77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+ 6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl +zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0 ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0 uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+ FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7 jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/ u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1 puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH 6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/ c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6 5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9 xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ 2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8 aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/ OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+ hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl pYYsfPQS -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK 8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB 95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn 8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ 2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG 9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta 3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk 6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 /qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 jVaMaA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA 2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu MdRAGmI0Nj81Aa6sY6A= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG 7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ qdq5snUb9kLy78fyGPmJvKP/iiMucEc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM 7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs yZyQ2uypQjyttgI= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7 XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1 JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51 b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV 9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7 kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS RGQDJereW26fyfJOrN3H -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx 3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK 4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv 2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 mfnGV/TJVTl4uix5yaaIK/QI -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9 hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM 1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws 6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50 aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u 7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0 xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn 0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM //bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t 3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 DzFc6PLZ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO 8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn jBJ7xUS0rg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u 7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY oJ2daZH9 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te 2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC /Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC 4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y 5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ 4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ +mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c 2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0 IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF 9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN /BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4 sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+ seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz 4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+ BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 7M2CYfE45k+XmCpajQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h 2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq 299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3 LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd 7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw ++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt 398znM/jra6O1I7mT1GvFpLgXPYHDw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd /ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv 2G0xffX8oRAHh84vWdw+WNs= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ O+7ETPTsJ3xCwnR8gooJybQDJbw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIIDjCCBfagAwIBAgIJAOiOtsn4KhQoMA0GCSqGSIb3DQEBBQUAMIG8MQswCQYD VQQGEwJVUzEQMA4GA1UECBMHSW5kaWFuYTEVMBMGA1UEBxMMSW5kaWFuYXBvbGlz MSgwJgYDVQQKEx9Tb2Z0d2FyZSBpbiB0aGUgUHVibGljIEludGVyZXN0MRMwEQYD VQQLEwpob3N0bWFzdGVyMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkx JTAjBgkqhkiG9w0BCQEWFmhvc3RtYXN0ZXJAc3BpLWluYy5vcmcwHhcNMDgwNTEz MDgwNzU2WhcNMTgwNTExMDgwNzU2WjCBvDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT B0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMfU29mdHdh cmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1hc3RlcjEe MBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcNAQkBFhZo b3N0bWFzdGVyQHNwaS1pbmMub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC CgKCAgEA3DbmR0LCxFF1KYdAw9iOIQbSGE7r7yC9kDyFEBOMKVuUY/b0LfEGQpG5 GcRCaQi/izZF6igFM0lIoCdDkzWKQdh4s/Dvs24t3dHLfer0dSbTPpA67tfnLAS1 fOH1fMVO73e9XKKTM5LOfYFIz2u1IiwIg/3T1c87Lf21SZBb9q1NE8re06adU1Fx Y0b4ShZcmO4tbZoWoXaQ4mBDmdaJ1mwuepiyCwMs43pPx93jzONKao15Uvr0wa8u jyoIyxspgpJyQ7zOiKmqp4pRQ1WFmjcDeJPI8L20QcgHQprLNZd6ioFl3h1UCAHx ZFy3FxpRvB7DWYd2GBaY7r/2Z4GLBjXFS21ZGcfSxki+bhQog0oQnBv1b7ypjvVp /rLBVcznFMn5WxRTUQfqzj3kTygfPGEJ1zPSbqdu1McTCW9rXRTunYkbpWry9vjQ co7qch8vNGopCsUK7BxAhRL3pqXTT63AhYxMfHMgzFMY8bJYTAH1v+pk1Vw5xc5s zFNaVrpBDyXfa1C2x4qgvQLCxTtVpbJkIoRRKFauMe5e+wsWTUYFkYBE7axt8Feo +uthSKDLG7Mfjs3FIXcDhB78rKNDCGOM7fkn77SwXWfWT+3Qiz5dW8mRvZYChD3F TbxCP3T9PF2sXEg2XocxLxhsxGjuoYvJWdAY4wCAs1QnLpnwFVMCAwEAAaOCAg8w ggILMB0GA1UdDgQWBBQ0cdE41xU2g0dr1zdkQjuOjVKdqzCB8QYDVR0jBIHpMIHm gBQ0cdE41xU2g0dr1zdkQjuOjVKdq6GBwqSBvzCBvDELMAkGA1UEBhMCVVMxEDAO BgNVBAgTB0luZGlhbmExFTATBgNVBAcTDEluZGlhbmFwb2xpczEoMCYGA1UEChMf U29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDETMBEGA1UECxMKaG9zdG1h c3RlcjEeMBwGA1UEAxMVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSUwIwYJKoZIhvcN AQkBFhZob3N0bWFzdGVyQHNwaS1pbmMub3JnggkA6I62yfgqFCgwDwYDVR0TAQH/ BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAAcwCQYDVR0SBAIwADAuBglghkgBhvhC AQ0EIRYfU29mdHdhcmUgaW4gdGhlIFB1YmxpYyBJbnRlcmVzdDAwBglghkgBhvhC AQQEIxYhaHR0cHM6Ly9jYS5zcGktaW5jLm9yZy9jYS1jcmwucGVtMDIGCWCGSAGG +EIBAwQlFiNodHRwczovL2NhLnNwaS1pbmMub3JnL2NlcnQtY3JsLnBlbTAhBgNV HREEGjAYgRZob3N0bWFzdGVyQHNwaS1pbmMub3JnMA4GA1UdDwEB/wQEAwIBBjAN BgkqhkiG9w0BAQUFAAOCAgEAtM294LnqsgMrfjLp3nI/yUuCXp3ir1UJogxU6M8Y PCggHam7AwIvUjki+RfPrWeQswN/2BXja367m1YBrzXU2rnHZxeb1NUON7MgQS4M AcRb+WU+wmHo0vBqlXDDxm/VNaSsWXLhid+hoJ0kvSl56WEq2dMeyUakCHhBknIP qxR17QnwovBc78MKYiC3wihmrkwvLo9FYyaW8O4x5otVm6o6+YI5HYg84gd1GuEP sTC8cTLSOv76oYnzQyzWcsR5pxVIBcDYLXIC48s9Fmq6ybgREOJJhcyWR2AFJS7v dVkz9UcZFu/abF8HyKZQth3LZjQl/GaD68W2MEH4RkRiqMEMVObqTFoo5q7Gt/5/ O5aoLu7HaD7dAD0prypjq1/uSSotxdz70cbT0ZdWUoa2lOvUYFG3/B6bzAKb1B+P +UqPti4oOxfMxaYF49LTtcYDyeFIQpvLP+QX4P4NAZUJurgNceQJcHdC2E3hQqlg g9cXiUPS1N2nGLar1CQlh7XU4vwuImm9rWgs/3K1mKoGnOcqarihk3bOsPN/nOHg T7jYhkalMwIsJWE3KpLIrIF0aGOHM3a9BX9e1dUCbb2v/ypaqknsmHlHU5H2DjRa yaXG67Ljxay2oHA1u8hRadDytaIybrw/oDc5fHE2pgXfDBLkFqfF1stjo5VwP+YE o2A= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ 0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA 7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH 7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX 4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ 51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ 4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr 6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN 9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h 9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo +fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h 3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX 0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c /3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D 34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv 033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq 4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK /yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD 3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE 7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb 7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka +elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z 7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs 4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG 52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy wy39FCqQmbkHzJ8= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I 0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo IhNzbM8m9Yop5w== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv 6pZjamVFkpUBtA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI 2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx 1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV 5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY 1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 sycX -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t 9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd +SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N 0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie 4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp /hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y Johw1+qRzT65ysCQblrGXnRl11z+o+I= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp 3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB 8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R 85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm 4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y /X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE 1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1 Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+ BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8 zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6 Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5 XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c 77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3 +GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6 FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD 75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp 5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p 6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI l7WdmplNsDz4SgCbZN2fOUvRJ9e4 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH 0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ 6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS 1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB 3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh 4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc 3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz 8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l 7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE +V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR /xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP 0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf 3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl 8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy 1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGGTCCBAGgAwIBAgIIPtVRGeZNzn4wDQYJKoZIhvcNAQELBQAwajEhMB8GA1UE AxMYU0cgVFJVU1QgU0VSVklDRVMgUkFDSU5FMRwwGgYDVQQLExMwMDAyIDQzNTI1 Mjg5NTAwMDIyMRowGAYDVQQKExFTRyBUUlVTVCBTRVJWSUNFUzELMAkGA1UEBhMC RlIwHhcNMTAwOTA2MTI1MzQyWhcNMzAwOTA1MTI1MzQyWjBqMSEwHwYDVQQDExhT RyBUUlVTVCBTRVJWSUNFUyBSQUNJTkUxHDAaBgNVBAsTEzAwMDIgNDM1MjUyODk1 MDAwMjIxGjAYBgNVBAoTEVNHIFRSVVNUIFNFUlZJQ0VTMQswCQYDVQQGEwJGUjCC AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoVgLsfJXwTukK0rcHoyKL ULO5Lhk9V9sZqtIr5M5C4myh5F0lHjMdtkXRtPpZilZwyW0IdmlwmubHnAgwE/7m 0ZJoYT5MEfJu8rF7V1ZLCb3cD9lxDOiaN94iEByZXtaxFwfTpDktwhpz/cpLKQfC eSnIyCauLMT8I8hL4oZWDyj9tocbaF85ZEX9aINsdSQePHWZYfrSFPipS7HYfad4 0hNiZbXWvn5qA7y1svxkMMPQwpk9maTTzdGxxFOHe0wTE2Z/v9VlU2j5XB7ltP82 mUWjn2LAfxGCAVTeD2WlOa6dSEyJoxA74OaD9bDaLB56HFwfAKzMq6dgZLPGxXvH VUZ0PJCBDkqOWZ1UsEixUkw7mO6r2jS3U81J2i/rlb4MVxH2lkwEeVyZ1eXkvm/q R+5RS+8iJq612BGqQ7t4vwt+tN3PdB0lqYljseI0gcSINTjiAg0PE8nVKoIV8IrE QzJW5FMdHay2z32bll0eZOl0c8RW5BZKUm2SOdPhTQ4/YrnerbUdZbldUv5dCamc tKQM2S9FdqXPjmqanqqwEaHrYcbrPx78ZrQSnUZ/MhaJvnFFr5Eh2f2Tv7QCkUL/ SR/tixVo3R+OrJvdggWcRGkWZBdWX0EPSk8ED2VQhpOX7EW/XcIc3M/E2DrmeAXQ xVVVqV7+qzohu+VyFPcLAgMBAAGjgcIwgb8wHQYDVR0OBBYEFCkgy/HDD9oGjhOT h/5fYBopu/O2MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUKSDL8cMP2gaO E5OH/l9gGim787YwEQYDVR0gBAowCDAGBgRVHSAAMEkGA1UdHwRCMEAwPqA8oDqG OGh0dHA6Ly9jcmwuc2d0cnVzdHNlcnZpY2VzLmNvbS9yYWNpbmUtR3JvdXBlU0cv TGF0ZXN0Q1JMMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEATEZn 4ERQ9cW2urJRCiUTHbfHiC4fuStkoMuTiFJZqmD1zClSF/8E5ze0MRFGfisebKeL PEeaXvSqXZA7RT2fSsmKe47A7j55i5KjyJRKuCgRa6YlX129x8j7g09VMeZc8BN8 471/Kiw3N5RJr4QfFCeiWBCPCjk3GhIgQY8Z9qkfGe2yNLKtfTNEi18KB0PydkVF La3kjQ4A/QQIqudr+xe9sAhWDjUqcvCz5006Tw3c82ASszhkjNv54SaNL+9O6CRH PjY0imkPKGuLh8a9hSb50+tpIVZgkdb34GLCqHGuLt5mI7VSRqakSDcsfwEWVxH3 Jw0O5Q/WkEXhHj8h3NL8FhgTPk1qsiZqQF4leP049KxYejcbmEAEx47J1MRnYbGY rvDNDty5r2WDewoEij9hqvddQYbmxkzCTzpcVuooO6dEz8hKZPVyYC3jQ7hK4HU8 MuSqFtcRucFF2ZtmY2blIrc07rrVdC8lZPOBVMt33lfUk+OsBzE6PlwDg1dTx/D+ aNglUE0SyObhlY1nqzyTPxcCujjXnvcwpT09RAEzGpqfjtCf8e4wiHPvriQZupdz FcHscQyEZLV77LxpPqRtCRY2yko5isune8YdfucziMm+MG2chZUh6Uc7Bn6B4upG 5nBYgOao8p0LadEziVkw82TTC/bOKwn7fRB2LhA= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w +2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B 26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst 0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK 1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ 8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm fyWl8kgAwKQB2j8= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM 0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K 2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl 6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK 9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r 0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f 2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL 6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv /2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N 8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 wSsSnqaeG8XmDtkx2Q== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD 1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ 5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f 46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth 7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm 7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb I+2ksx0WckNLIOFZfsLorSa/ovc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ /jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs 81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG 9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx 0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+ AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA 0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/ BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01 GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4 1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN ZetX2fNXlrtIzYE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi 1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN 9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP BSeOE6Fuwg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN 8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ 1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT 91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG +7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M 733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF 10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz 0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc 46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm 4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB /zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL 1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh 15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW 6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy KwbQBM0= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i 2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ 2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1 9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb +gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4 WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj /feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6 jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2 ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1 2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2 ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/ BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+ J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT ee5Ehr7XHuQe+w== -----END CERTIFICATE----- ================================================ FILE: build/homebrew.sh ================================================ #!/bin/bash # # homebrew.sh creates an updated homebrew on fabio set -o nounset set -o errexit set -o pipefail readonly prgdir=$(cd $(dirname $0); pwd) readonly brewdir=$(brew tap-info homebrew/core | head -n 2 | tail -n 1 | sed 's/[[:space:]].*$//') v=${1:-} [[ -n "$v" ]] || read -p "Enter version (e.g. 1.0.4): " v if [[ -z "$v" ]] ; then echo "Usage: $0 (e.g. 1.0.4)" exit 1 fi v=${v/v/} srcurl=https://github.com/fabiolb/fabio/archive/v${v}.tar.gz shasum=$(wget -O- -q "$srcurl" | shasum -a 256 | awk '{ print $1; }') echo -e "/url DAurl \"$srcurl\"/sha256 DAsha256 \"$shasum\":wq" > $prgdir/homebrew.vim brew update brew update ( cd $brewdir git checkout -b fabio-$v origin/master vim -u NONE -s $prgdir/homebrew.vim $brewdir/Formula/fabio.rb brew install --build-from-source fabio brew test fabio brew install fabio brew audit --strict fabio git add Formula/fabio.rb git commit -m "fabio $v" git push --set-upstream fabiolb fabio-$v ) echo "Goto https://github.com/fabiolb/homebrew-core to create pull request" open https://github.com/fabiolb/homebrew-core exit 0 ================================================ FILE: build/homebrew.vim ================================================ /url DAurl "https://github.com/fabiolb/fabio/archive/v1.6.3.tar.gz"/sha256 DAsha256 "e85b70a700652b051260b8c49ce63d21d2579517601a91d893a7fa9444635ad3":wq ================================================ FILE: build/issue-225-gen-cert.bash ================================================ #!/bin/bash # # This script addresses issue #225 (https://github.com/fabiolb/fabio/issues/225) # and generates a number of certificates for testing fabio with client # certificate authentication with signed client certificates. # # First, a self-signed CA certificate is created which is used to sign both the # server and client certificates. Then a server and a client certificate are # created. The demo/cert/{ca,client,server} directories contain the generated # certificates and their private keys. # # Second, a directory structure for a fabio path cert store is created under # demo/cert/fabio/{client,server}. The server directory contains the TLS server # certificate and private key. The client directory contains the client # certificate **and** the CA certificate (no private keys). Including the CA # certificate is necessary since otherwise fabio (or the go crypto library) # cannot verify the client certificate and will respond with the following # error. Try this by removing the demo/cert/fabio/client/ca-cert.pem file and # restart fabio. # # http: TLS handshake error from 127.0.0.1:53272: tls: failed to verify client's certificate: x509: certificate signed by unknown authority # set -o errexit set -o nounset basedir=$(cd $(dirname $0)/.. ; pwd) certdir=$basedir/demo/cert openssl=$(which openssl) [[ -x /usr/local/opt/openssl/bin/openssl ]] && openssl=/usr/local/opt/openssl/bin/openssl # shorten certdir certdir=${certdir/$(pwd)\//} [[ -z "$certdir" ]] && (echo "certdir empty" ; exit 1) [[ -d "$certdir" ]] && rm -rf "$certdir" mkdir -p $certdir/{ca,client,server} $certdir/fabio/{client,server} echo "generate CA cert" $openssl req \ -x509 -nodes -days 365 -sha256 -newkey rsa:2048 \ -subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=ca' \ -keyout "$certdir/ca/ca-key.pem" -out "$certdir/ca/ca-cert.pem" echo "generate client cert" $openssl req \ -nodes -days 365 -sha256 -newkey rsa:2048 \ -subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=client' \ -keyout $certdir/client/client-key.pem -out $certdir/client/client.csr $openssl x509 \ -req -set_serial 02 -CA $certdir/ca/ca-cert.pem -CAkey $certdir/ca/ca-key.pem \ -in $certdir/client/client.csr -out $certdir/client/client-cert.pem echo "generate server cert" $openssl req \ -nodes -days 365 -sha256 -newkey rsa:2048 \ -subj '/C=NL/ST=Noord-Holland/L=Amsterdam/CN=www.server.com' \ -keyout $certdir/server/server-key.pem -out $certdir/server/server.csr $openssl x509 \ -req -set_serial 03 -CA $certdir/ca/ca-cert.pem -CAkey $certdir/ca/ca-key.pem \ -in $certdir/server/server.csr -out $certdir/server/server-cert.pem cp $certdir/ca/ca-cert.pem $certdir/fabio/client cp $certdir/client/client-cert.pem $certdir/fabio/client cp $certdir/server/server-{cert,key}.pem $certdir/fabio/server cat<; if (/^### \[$ENV{RELEASE}.*?\n\s*(.*?)^### \[v/gms) { print $1; } ================================================ FILE: build/tag.sh ================================================ #!/bin/bash -e # # Script for replacing the version number # in main.go, committing and tagging the code readonly prgdir=$(cd $(dirname $0); pwd) readonly basedir=$(cd $prgdir/..; pwd) v=$1 [[ -n "$v" ]] || read -p "Enter version (e.g. 1.0.4): " v if [[ -z "$v" ]]; then echo "Usage: $0 " exit 1 fi grep -q "$v" CHANGELOG.md || echo "CHANGELOG.md not updated" read -p "Tag fabio version $v? (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi sed -i '' -e "s|^var version .*$|var version = \"$v\"|" $basedir/main.go git add $basedir/main.go git commit -S -m "Release v$v" || true git tag -s v$v -m "Tag v${v}" remote=$2 [[ -n "$origin" ]] || read -p "Enter remote (e.g. origin): " origin if [[ -z "$origin" ]]; then echo "Usage: $0 " exit 1 fi git push $remote $version ================================================ FILE: build/update-ssl.sh ================================================ #!/bin/bash -e # # Script for updating ca-certificates.crt prgdir=$(cd $(dirname $0) ; pwd) echo "Updating certificates" sudo update-ca-certificates cp /etc/ssl/certs/ca-certificates.crt $prgdir ================================================ FILE: cert/consul_source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "errors" "fmt" "log" "net/url" "path" "reflect" "strings" "time" "github.com/hashicorp/consul/api" ) // ConsulSource implements a certificate source which loads // TLS and client authentication certificates from the consul KV store. // The CertURL/ClientCAURL must point to the base path of the certificates. // The TLS certificates are updated automatically when the KV store // changes. type ConsulSource struct { CertURL string ClientCAURL string CAUpgradeCN string } func parseConsulURL(rawurl string) (config *api.Config, key string, err error) { if rawurl == "" || !strings.HasPrefix(rawurl, "http://") && !strings.HasPrefix(rawurl, "https://") { return nil, "", errors.New("invalid url") } u, err := url.Parse(rawurl) if err != nil { return nil, "", err } config = &api.Config{Address: u.Host, Scheme: u.Scheme} if len(u.Query()["token"]) > 0 { config.Token = u.Query()["token"][0] } // path needs to point to kv store and we need // to strip the prefix off to get the key const prefix = "/v1/kv/" key = u.Path if !strings.HasPrefix(key, prefix) { return nil, "", errors.New("missing prefix: " + prefix) } key = key[len(prefix):] return } func (s ConsulSource) LoadClientCAs() (*x509.CertPool, error) { if s.ClientCAURL == "" { return nil, nil } config, key, err := parseConsulURL(s.ClientCAURL) if err != nil { return nil, err } client, err := api.NewClient(config) if err != nil { return nil, err } load := func(key string) (map[string][]byte, error) { pemBlocks, _, err := getCerts(client, key, 0) return pemBlocks, err } return newCertPool(key, s.CAUpgradeCN, load) } func (s ConsulSource) Certificates() chan []tls.Certificate { if s.CertURL == "" { return nil } config, key, err := parseConsulURL(s.CertURL) if err != nil { log.Printf("[ERROR] cert: Failed to parse consul url. %s", err) } client, err := api.NewClient(config) if err != nil { log.Printf("[ERROR] cert: Failed to create consul client. %s", err) } pemBlocksCh := make(chan map[string][]byte, 1) go watchKV(client, key, pemBlocksCh) ch := make(chan []tls.Certificate, 1) go func() { for pemBlocks := range pemBlocksCh { certs, err := loadCertificates(pemBlocks) if err != nil { log.Printf("[ERROR] cert: Failed to load certificates. %s", err) continue } ch <- certs } }() return ch } // watchKV monitors a key in the KV store for changes. func watchKV(client *api.Client, key string, pemBlocks chan map[string][]byte) { var lastIndex uint64 var lastValue map[string][]byte for { value, index, err := getCerts(client, key, lastIndex) if err != nil { log.Printf("[WARN] cert: Error fetching certificates from %s. %v", key, err) time.Sleep(time.Second) continue } if !reflect.DeepEqual(value, lastValue) || index != lastIndex { log.Printf("[DEBUG] cert: Certificate index changed to #%d", index) pemBlocks <- value lastValue, lastIndex = value, index } } } func getCerts(client *api.Client, key string, waitIndex uint64) (pemBlocks map[string][]byte, lastIndex uint64, err error) { q := &api.QueryOptions{RequireConsistent: true, WaitIndex: waitIndex} kvpairs, meta, err := client.KV().List(key, q) if err != nil { return nil, 0, fmt.Errorf("consul: list: %s", err) } if len(kvpairs) == 0 { return nil, meta.LastIndex, nil } pemBlocks = map[string][]byte{} for _, kvpair := range kvpairs { pemBlocks[path.Base(kvpair.Key)] = kvpair.Value } return pemBlocks, meta.LastIndex, nil } ================================================ FILE: cert/consul_source_test.go ================================================ package cert import ( "reflect" "testing" "github.com/hashicorp/consul/api" ) func TestParseConsulURL(t *testing.T) { tests := []struct { name string in string config *api.Config key string errstr string }{ { name: "empty url", errstr: "invalid url", }, { name: "invalid url", in: "this is not a url", errstr: "invalid url", }, { name: "no kv store url", in: "http://localhost:8500/path/to/cert", errstr: "missing prefix: /v1/kv/", }, { name: "url without token", in: "http://localhost:8500/v1/kv/path/to/cert", config: &api.Config{Address: "localhost:8500", Scheme: "http"}, key: "path/to/cert", }, { name: "https url", in: "https://localhost:8500/v1/kv/path/to/cert", config: &api.Config{Address: "localhost:8500", Scheme: "https"}, key: "path/to/cert", }, { name: "url with token", in: "http://localhost:8500/v1/kv/path/to/cert?token=123", config: &api.Config{Address: "localhost:8500", Scheme: "http", Token: "123"}, key: "path/to/cert", }, } for _, tt := range tests { tt := tt // capture loop var t.Run(tt.name, func(t *testing.T) { config, key, err := parseConsulURL(tt.in) var errstr string if err != nil { errstr = err.Error() } if got, want := errstr, tt.errstr; got != want { t.Fatalf("got err %q want %q", got, want) } if errstr != "" || tt.errstr != "" { return } if got, want := key, tt.key; got != want { t.Errorf("got key %q want %q", got, want) } if got, want := config, tt.config; !reflect.DeepEqual(got, want) { t.Errorf("got config %+v want %+v", got, want) } }) } } ================================================ FILE: cert/file_source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "os" "github.com/fabiolb/fabio/exit" ) // FileSource implements a certificate source for one // TLS and one client authentication certificate. // The certificates are loaded during startup and are cached // in memory until the program exits. // It exists to support the legacy configuration only. The // PathSource should be used instead. type FileSource struct { CertFile string KeyFile string ClientAuthFile string CAUpgradeCN string } func (s FileSource) LoadClientCAs() (*x509.CertPool, error) { return newCertPool(s.ClientAuthFile, s.CAUpgradeCN, func(path string) (map[string][]byte, error) { if s.ClientAuthFile == "" { return nil, nil } pemBlock, err := os.ReadFile(path) return map[string][]byte{path: pemBlock}, err }) } func (s FileSource) Certificates() chan []tls.Certificate { ch := make(chan []tls.Certificate, 1) ch <- []tls.Certificate{loadX509KeyPair(s.CertFile, s.KeyFile)} close(ch) return ch } func loadX509KeyPair(certFile, keyFile string) tls.Certificate { if certFile == "" { exit.Fatalf("[FATAL] cert: CertFile is required") } if keyFile == "" { keyFile = certFile } cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { exit.Fatalf("[FATAL] cert: Error loading certificate. %s", err) } return cert } ================================================ FILE: cert/http_source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "time" ) // HTTPSource implements a certificate source which loads // TLS and client authentication certificates from an HTTP/HTTPS server. // The CertURL/ClientCAURL must point to a text file in the directory // of the certificates. The text file contains all files that should // be loaded from this directory - one filename per line. // The TLS certificates are updated automatically when Refresh // is not zero. Refresh cannot be less than one second to prevent // busy loops. type HTTPSource struct { CertURL string ClientCAURL string CAUpgradeCN string Refresh time.Duration } func (s HTTPSource) LoadClientCAs() (*x509.CertPool, error) { return newCertPool(s.ClientCAURL, s.CAUpgradeCN, loadURL) } func (s HTTPSource) Certificates() chan []tls.Certificate { ch := make(chan []tls.Certificate, 1) go watch(ch, s.Refresh, s.CertURL, loadURL) return ch } ================================================ FILE: cert/load.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "io" "log" "net/http" "net/url" "os" "path" "path/filepath" "sort" "strings" ) // MaxSize is the limit for individual certificate sizes. const MaxSize = 1 << 20 // 1MB func loadURL(listURL string) (pemBlocks map[string][]byte, err error) { if listURL == "" { return nil, nil } baseURL, err := base(listURL) if err != nil { return nil, fmt.Errorf("cert: %s", err) } fetch := func(url string) (buf []byte, err error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } // fetch the file with the list of filenames list, err := fetch(listURL) if err != nil { return nil, fmt.Errorf("cert: %s", err) } // fetch the individual files pemBlocks = map[string][]byte{} for _, p := range strings.Split(string(list), "\n") { if p == "" { continue } path := baseURL + p buf, err := fetch(path) if err != nil { return nil, fmt.Errorf("cert: %s", err) } pemBlocks[path] = buf } return pemBlocks, nil } func loadPath(root string) (pemBlocks map[string][]byte, err error) { if root == "" { return nil, nil } pemBlocks = map[string][]byte{} err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { // check if the root directory exists if _, ok := err.(*os.PathError); ok && path == root { return nil } if err != nil { return err } if info.IsDir() || filepath.Ext(info.Name()) != ".pem" || strings.HasPrefix(info.Name(), ".") { return nil } if info.Size() > MaxSize { log.Printf("[WARN] cert: File too large %s", info.Name()) return nil } buf, err := os.ReadFile(path) if err != nil { return fmt.Errorf("cert: %s", err) } pemBlocks[path] = buf return nil }) if err != nil { return nil, err } return pemBlocks, nil } func loadCertificates(pemBlocks map[string][]byte) ([]tls.Certificate, error) { var errs []error var n []string x := map[string]tls.Certificate{} for name := range pemBlocks { var certFile, keyFile string switch { case strings.HasSuffix(name, "-cert.pem"): certFile, keyFile = name, replaceSuffix(name, "-cert.pem", "-key.pem") case strings.HasSuffix(name, "-key.pem"): certFile, keyFile = replaceSuffix(name, "-key.pem", "-cert.pem"), name case strings.HasSuffix(name, ".pem"): certFile, keyFile = name, name default: continue } if _, exists := x[certFile]; exists { continue } cert, key := pemBlocks[certFile], pemBlocks[keyFile] if cert == nil || key == nil { errs = append(errs, fmt.Errorf("cert: cannot load certificate %s", name)) continue } c, err := tls.X509KeyPair(cert, key) if err != nil { errs = append(errs, fmt.Errorf("cert: invalid certificate %s. %s", name, err)) continue } x[certFile] = c n = append(n, certFile) } // append certificates in alphabetical order of the // cert filenames. This determines which certificate // becomes the default certificate (the first one) sort.Strings(n) var certs []tls.Certificate for _, certFile := range n { certs = append(certs, x[certFile]) } return certs, errors.Join(errs...) } // base returns the rawurl with the last element of the path // removed. http://foo.com/x/y becomes http://foo.com/x func base(rawurl string) (string, error) { if rawurl == "" { return "", nil } u, err := url.Parse(rawurl) if err != nil { return "", err } if u.Path != "/" { u.Path = path.Dir(u.Path) } return u.String(), nil } // replaceSuffix replaces oldSuffix with newSuffix in s. // It is only valid when s has oldSuffix and oldSuffix is not empty. func replaceSuffix(s string, oldSuffix, newSuffix string) string { return s[:len(s)-len(oldSuffix)] + newSuffix } // newCertPool creates a new x509.CertPool by loading the // PEM blocks from loadFn(path) and adding them to a CertPool. func newCertPool(path string, caUpgradeCN string, loadFn func(path string) (pemBlocks map[string][]byte, err error)) (*x509.CertPool, error) { pemBlocks, err := loadFn(path) if err != nil { return nil, err } if len(pemBlocks) == 0 { return nil, nil } pool := x509.NewCertPool() for _, pemBlock := range pemBlocks { for p, rest := pem.Decode(pemBlock); p != nil; p, rest = pem.Decode(rest) { cert, err := x509.ParseCertificate(p.Bytes) if err != nil { return nil, err } upgradeCACertificate(cert, caUpgradeCN) pool.AddCert(cert) } } log.Printf("[INFO] cert: Load client CA certs from %s", path) return pool, nil } // upgradeCACertificate upgrades a certificate to a self-signing CA certificate if the CN matches. // Issue #108: Allow generated AWS API Gateway certs to be used for client cert authentication func upgradeCACertificate(cert *x509.Certificate, caUpgradeCN string) { if caUpgradeCN != "" && caUpgradeCN == cert.Issuer.CommonName { cert.BasicConstraintsValid = true cert.IsCA = true cert.KeyUsage = x509.KeyUsageCertSign log.Printf("[INFO] cert: Upgrading cert %s to CA cert", cert.Issuer.CommonName) } } ================================================ FILE: cert/load_test.go ================================================ package cert import ( "crypto/x509" "encoding/pem" "testing" ) func TestBase(t *testing.T) { tests := []struct { in, out, err string }{ {"", "", ""}, {"http://foo.com/x/y", "http://foo.com/x", ""}, {"http://foo.com/x/y?p=q", "http://foo.com/x?p=q", ""}, } for i, tt := range tests { u, err := base(tt.in) if err != nil { if got, want := err.Error(), tt.err; got != want { t.Errorf("%d: got %v want %v", i, got, want) continue } } if tt.err != "" { t.Errorf("%d: got nil want %v", i, tt.err) continue } if got, want := u, tt.out; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } } func TestReplaceSuffix(t *testing.T) { if got, want := replaceSuffix("ab", "b", "c"), "ac"; got != want { t.Errorf("got %q want %q", got, want) } } func TestUpgradeCACertificate(t *testing.T) { // generated at // https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/client-certificates const awsAPIGWCert = ` -----BEGIN CERTIFICATE----- MIIC6DCCAdCgAwIBAgIIZAgycYqDRqQwDQYJKoZIhvcNAQELBQAwNDELMAkGA1UE BhMCVVMxEDAOBgNVBAcTB1NlYXR0bGUxEzARBgNVBAMTCkFwaUdhdGV3YXkwHhcN MTYwNzEyMTkzMTMwWhcNMTcwNzEyMTkzMTMwWjA0MQswCQYDVQQGEwJVUzEQMA4G A1UEBxMHU2VhdHRsZTETMBEGA1UEAxMKQXBpR2F0ZXdheTCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAM/a0LKQd/obIwcKu09EjlHP4b7QqmK/JnJfd1Eq m6We85FGu+26s7+Bpw1xiyK2jFuzQ4JFyXVkWLJH8e3Mp7P91MvJ1x6UCRk+Fz6Q Lauw5SBVmDO5CauB4NcICTYEeTT3c0m8t6sDpHf+DHZC87gq9rhBggKXfNO3ntWw Kq2uGscvnOz2/n2XIucFf2U7GI/cOapGXvIyrB5e/swSCyNkgOJ2HekzWjprxSs5 zu9JSOIzejgm8+/nnPOO9ycVrjN3qazUEXfF1QdvZeNCZ9GL6ZICAYo9xnnNLJnW 6p5d0Fw6U+V/nlNpgCB5djTwXaY51ScoW/i3ukHBZe9QIEcCAwEAATANBgkqhkiG 9w0BAQsFAAOCAQEAzwUJlSv/9XVoeCbot+3mdviZI5B7VnEKGl2Oam1fQzGZkkzB kqBgtRrHux3BRxPRqS4jM4akdplFhejHExVatOxfS+DEXzFefi+aMb7qApB1YjV/ 5FIIQdZaVOlw2KIRXCy04nxrKJmJ1T5RCkYC80dYpNfmDb5REUtp8jU78/Schsx7 0nCsrWkBSO1QtR4NnBlHbEM+imh3aCQz23SUK5Q/NTe4r2pu0zUl5b2YNgefvWle 7fe6T137rmhji9K+tYNznLGk0XmiguQPM2qJLxqeVQsA32wUbbSIFWH+KsXRPfpU n/iFVG4Y6zyXQY2RzTt+ZB2VPR72X4wqS9fBeQ== -----END CERTIFICATE-----` p, rest := pem.Decode([]byte(awsAPIGWCert)) if len(rest) > 0 { t.Fatal("want only one cert") } cert, err := x509.ParseCertificate(p.Bytes) if err != nil { t.Fatal(err) } // check that cert does not have the flags set if got, want := cert.BasicConstraintsValid, false; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.IsCA, false; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.KeyUsage, x509.KeyUsage(0); got != want { t.Fatalf("got %v want %v", got, want) } // run upgrade with not-matching CN expecting no change upgradeCACertificate(cert, "no match") if got, want := cert.BasicConstraintsValid, false; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.IsCA, false; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.KeyUsage, x509.KeyUsage(0); got != want { t.Fatalf("got %v want %v", got, want) } // run upgrade with matching CN upgradeCACertificate(cert, "ApiGateway") if got, want := cert.BasicConstraintsValid, true; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.IsCA, true; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := cert.KeyUsage, x509.KeyUsageCertSign; got != want { t.Fatalf("got %v want %v", got, want) } } ================================================ FILE: cert/path_source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "path/filepath" "time" ) const ( DefaultCertPath = "cert" DefaultClientCAPath = "clientca" ) type PathSource struct { Path string CertPath string ClientCAPath string CAUpgradeCN string Refresh time.Duration } func (s PathSource) LoadClientCAs() (*x509.CertPool, error) { path := makePath(s.Path, s.ClientCAPath, DefaultClientCAPath) return newCertPool(path, s.CAUpgradeCN, loadPath) } func (s PathSource) Certificates() chan []tls.Certificate { path := makePath(s.Path, s.CertPath, DefaultCertPath) ch := make(chan []tls.Certificate, 1) go watch(ch, s.Refresh, path, loadPath) return ch } func makePath(parent, child, defaultChild string) string { if child == "" { return filepath.Join(parent, defaultChild) } return filepath.Join(parent, child) } ================================================ FILE: cert/source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "fmt" "github.com/fabiolb/fabio/config" "golang.org/x/sync/singleflight" ) // Source provides the interface for dynamic certificate sources. type Source interface { // Certificates() loads certificates for TLS connections. // The first certificate is used as the default certificate // if the client does not support SNI or no matching certificate // could be found. TLS certificates can be updated at runtime. Certificates() chan []tls.Certificate // LoadClientCAs() provides certificates for client certificate // authentication. LoadClientCAs() (*x509.CertPool, error) } // Issuer is the interface implemented by sources that can issue certificates // on-demand. type Issuer interface { // Issue issues a new certificate for the given common name. Issue must // return a certificate or an error, never (nil, nil). Issue(commonName string) (*tls.Certificate, error) } // NewSource generates a cert source from the config options. func NewSource(cfg config.CertSource) (Source, error) { switch cfg.Type { case "file": return FileSource{ CertFile: cfg.CertPath, KeyFile: cfg.KeyPath, ClientAuthFile: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, }, nil case "path": return PathSource{ CertPath: cfg.CertPath, ClientCAPath: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, Refresh: cfg.Refresh, }, nil case "http": return HTTPSource{ CertURL: cfg.CertPath, ClientCAURL: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, Refresh: cfg.Refresh, }, nil case "consul": return ConsulSource{ CertURL: cfg.CertPath, ClientCAURL: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, }, nil case "vault": return &VaultSource{ CertPath: cfg.CertPath, ClientCAPath: cfg.ClientCAPath, CAUpgradeCN: cfg.CAUpgradeCN, Refresh: cfg.Refresh, Client: NewVaultClient(cfg.VaultFetchToken), }, nil case "vault-pki": src := NewVaultPKISource() src.CertPath = cfg.CertPath src.ClientCAPath = cfg.ClientCAPath src.CAUpgradeCN = cfg.CAUpgradeCN src.Refresh = cfg.Refresh src.Client = NewVaultClient(cfg.VaultFetchToken) return src, nil default: return nil, fmt.Errorf("invalid certificate source %q", cfg.Type) } } // TLSConfig creates a tls.Config which sets the GetCertificate field to a // certificate store which uses the given source to update the the certificates // on-demand. // // It also sets the ClientCAs field if src.LoadClientCAs returns a non-nil // value and sets ClientAuth to RequireAndVerifyClientCert. func TLSConfig(src Source, strictMatch bool, minVersion, maxVersion uint16, cipherSuites []uint16) (*tls.Config, error) { clientCAs, err := src.LoadClientCAs() if err != nil { return nil, err } sf := &singleflight.Group{} store := NewStore() x := &tls.Config{ MinVersion: minVersion, MaxVersion: maxVersion, CipherSuites: cipherSuites, NextProtos: []string{"h2", "http/1.1"}, GetCertificate: func(clientHello *tls.ClientHelloInfo) (cert *tls.Certificate, err error) { cert, err = getCertificate(store.certstore(), clientHello, strictMatch) if cert != nil { return } switch err { case nil, ErrNoCertsStored: // Store doesn't contain a suitable cert. Perhaps the source can issue one? default: // an unrecoverable error return } ca, ok := src.(Issuer) if !ok { return } serverName := clientHello.ServerName x, err, _ := sf.Do(serverName, func() (interface{}, error) { return ca.Issue(serverName) }) if err != nil { return cert, err } return x.(*tls.Certificate), nil }, } if clientCAs != nil { x.ClientCAs = clientCAs x.ClientAuth = tls.RequireAndVerifyClientCert } go func() { for certs := range src.Certificates() { store.SetCertificates(certs) } }() return x, nil } ================================================ FILE: cert/source_test.go ================================================ package cert import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "io" "log" "math/big" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "time" "golang.org/x/net/http2" "github.com/fabiolb/fabio/config" consulapi "github.com/hashicorp/consul/api" vaultapi "github.com/hashicorp/vault/api" "github.com/pascaldekloe/goe/verify" ) func TestTLSConfig(t *testing.T) { certPEM, keyPEM := makePEM("localhost", time.Minute) cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { t.Fatalf("X509KeyPair: got %s want nil", err) } pool := makeCertPool(certPEM) src := &StaticSource{cert, pool} tlsmin := uint16(0x1000) tlsmax := uint16(0x2000) tlsciphers := []uint16{0x1234, 0x5678} nextprotos := []string{"h2", "http/1.1"} cfg, err := TLSConfig(src, false, tlsmin, tlsmax, tlsciphers) if err != nil { t.Fatalf("got error %v want nil", err) } if got, want := cfg.MinVersion, tlsmin; got != want { t.Fatalf("got tls min version %04x want %04x", got, want) } if got, want := cfg.MaxVersion, tlsmax; got != want { t.Fatalf("got tls max version %04x want %04x", got, want) } if got, want := cfg.CipherSuites, tlsciphers; !reflect.DeepEqual(got, want) { t.Fatalf("got tls ciphers %v want %v", got, want) } if got, want := cfg.NextProtos, nextprotos; !reflect.DeepEqual(got, want) { t.Fatalf("got next protos %v want %v", got, want) } if got, want := cfg.ClientCAs, pool; got != want { t.Fatalf("got client CAs %v want %v", got, want) } if got, want := cfg.ClientAuth, tls.RequireAndVerifyClientCert; got != want { t.Fatalf("got client auth type %v want %v", got, want) } if cfg.GetCertificate == nil { t.Fatalf("got GetCertificate() nil want not nil") } } func TestNewSource(t *testing.T) { certsource := func(typ string) config.CertSource { return config.CertSource{ Type: typ, Name: "name", CertPath: "cert", KeyPath: "key", ClientCAPath: "clientca", CAUpgradeCN: "upgcn", Refresh: 3 * time.Second, Header: http.Header{"A": []string{"b"}}, } } tests := []struct { desc string cfg config.CertSource src Source err string }{ { desc: "invalid", cfg: config.CertSource{ Type: "invalid", }, src: nil, err: `invalid certificate source "invalid"`, }, { desc: "file", cfg: certsource("file"), src: FileSource{ CertFile: "cert", KeyFile: "key", ClientAuthFile: "clientca", CAUpgradeCN: "upgcn", }, }, { desc: "path", cfg: certsource("path"), src: PathSource{ CertPath: "cert", ClientCAPath: "clientca", CAUpgradeCN: "upgcn", Refresh: 3 * time.Second, }, }, { desc: "http", cfg: certsource("http"), src: HTTPSource{ CertURL: "cert", ClientCAURL: "clientca", CAUpgradeCN: "upgcn", Refresh: 3 * time.Second, }, }, { desc: "consul", cfg: certsource("consul"), src: ConsulSource{ CertURL: "cert", ClientCAURL: "clientca", CAUpgradeCN: "upgcn", }, }, { desc: "vault", cfg: certsource("vault"), src: &VaultSource{ Client: DefaultVaultClient, CertPath: "cert", ClientCAPath: "clientca", CAUpgradeCN: "upgcn", Refresh: 3 * time.Second, }, }, } for i, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { var errmsg string src, err := NewSource(tt.cfg) if err != nil { errmsg = err.Error() } if got, want := errmsg, tt.err; got != want { t.Fatalf("%d: got %q want %q", i, got, want) } got, want := src, tt.src verify.Values(t, "src", got, want) }) } } type StaticSource struct { cert tls.Certificate pool *x509.CertPool } func (s StaticSource) Certificates() chan []tls.Certificate { ch := make(chan []tls.Certificate, 1) ch <- []tls.Certificate{s.cert} close(ch) return ch } func (s StaticSource) LoadClientCAs() (*x509.CertPool, error) { return s.pool, nil } func TestStaticSource(t *testing.T) { certPEM, keyPEM := makePEM("localhost", time.Minute) cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { t.Fatalf("X509KeyPair: got %s want nil", err) } testSource(t, StaticSource{cert, nil}, makeCertPool(certPEM), 0) } func TestFileSource(t *testing.T) { dir := t.TempDir() defer os.RemoveAll(dir) certPEM, keyPEM := makePEM("localhost", time.Minute) certFile, keyFile := saveCert(dir, "localhost", certPEM, keyPEM) testSource(t, FileSource{CertFile: certFile, KeyFile: keyFile}, makeCertPool(certPEM), 0) } func TestPathSource(t *testing.T) { dir := t.TempDir() defer os.RemoveAll(dir) certPEM, keyPEM := makePEM("localhost", time.Minute) saveCert(dir, "localhost", certPEM, keyPEM) testSource(t, PathSource{CertPath: dir}, makeCertPool(certPEM), 10*time.Millisecond) } func TestHTTPSource(t *testing.T) { dir := t.TempDir() defer os.RemoveAll(dir) certPEM, keyPEM := makePEM("localhost", time.Minute) certFile, keyFile := saveCert(dir, "localhost", certPEM, keyPEM) listFile := filepath.Base(certFile) + "\n" + filepath.Base(keyFile) + "\n" writeFile(filepath.Join(dir, "list"), []byte(listFile)) srv := httptest.NewServer(http.FileServer(http.Dir(dir))) defer srv.Close() testSource(t, HTTPSource{CertURL: srv.URL + "/list"}, makeCertPool(certPEM), 500*time.Millisecond) } func TestConsulSource(t *testing.T) { const certURL = "http://127.0.0.1:8500/v1/kv/fabio/test/consul-server" // run a consul server if it isn't already running _, err := http.Get("http://127.0.0.1:8500/v1/status/leader") if err != nil { consul := os.Getenv("CONSUL_EXE") if consul == "" { consul = "consul" } version, err := exec.Command(consul, "--version").Output() if err != nil { t.Fatalf("Failed to run %s --version", consul) } cr := bytes.IndexRune(version, '\n') t.Logf("Starting %s: %s", consul, string(version[:cr])) start := time.Now() cmd := exec.Command(consul, "agent", "-bind", "127.0.0.1", "-server", "-dev") if err := cmd.Start(); err != nil { t.Fatalf("Failed to start consul server. %s", err) } defer cmd.Process.Kill() isUp := func() bool { resp, err := http.Get("http://127.0.0.1:8500/v1/status/leader") // /v1/status/leader returns '\n""' while consul is in leader election mode // and '"127.0.0.1:8300"' when not. So we punt by checking the // body length instead of the actual body content :) if err != nil { return false } defer resp.Body.Close() if resp.StatusCode != 200 { return false } n, err := io.Copy(io.Discard, resp.Body) return err == nil && n > 10 } // We need give consul ~8-10 seconds to become ready until I've // figured out whether we can speed this up. Make sure that this is // less than the global test timeout in Makefile. if !waitFor(12*time.Second, isUp) { t.Fatalf("Timeout waiting for consul server after %2.1f seconds", time.Since(start).Seconds()) } t.Logf("Consul is ready after %2.1f seconds", time.Since(start).Seconds()) } else { t.Log("Using existing consul server") } config, key, err := parseConsulURL(certURL) if err != nil { t.Fatalf("Failed to parse consul url: %s", err) } client, err := consulapi.NewClient(config) if err != nil { t.Fatalf("Failed to create consul client: %s", err) } defer func() { client.KV().DeleteTree(key, &consulapi.WriteOptions{}) }() write := func(name string, value []byte) { p := &consulapi.KVPair{Key: key + "/" + name, Value: value} _, err := client.KV().Put(p, &consulapi.WriteOptions{}) if err != nil { t.Fatalf("Failed to write %q to consul: %s", p.Key, err) } } certPEM, keyPEM := makePEM("localhost", time.Minute) write("localhost-cert.pem", certPEM) write("localhost-key.pem", keyPEM) testSource(t, ConsulSource{CertURL: certURL}, makeCertPool(certPEM), 500*time.Millisecond) } // vaultServer starts a vault server in dev mode and waits until is ready. func vaultServer(t *testing.T, addr, rootToken string) (*exec.Cmd, *vaultapi.Client) { vault := os.Getenv("VAULT_EXE") if vault == "" { vault = "vault" } version, err := exec.Command(vault, "--version").Output() if err != nil { t.Fatalf("Failed to run %s --version", vault) } t.Logf("Starting %s: %q", vault, string(version)) cmd := exec.Command(vault, "server", "-dev", "-dev-root-token-id="+rootToken, "-dev-listen-address="+addr) if err := cmd.Start(); err != nil { t.Fatalf("Failed to start vault server. %s", err) } c, err := vaultapi.NewClient(&vaultapi.Config{Address: "http://" + addr}) if err != nil { cmd.Process.Kill() t.Fatalf("NewClient failed: %s", err) } c.SetToken(rootToken) isUp := func() bool { ok, err := c.Sys().InitStatus() return err == nil && ok } if !waitFor(time.Second, isUp) { cmd.Process.Kill() t.Fatal("Timeout waiting for vault server") } policy := ` # Vault < 0.7 path "secret/fabio/cert" { capabilities = ["list"] } # Vault >= 0.7. Note the trailing slash. path "secret/fabio/cert/" { capabilities = ["list"] } path "secret/fabio/cert/*" { capabilities = ["read"] } # Vault >= 0.10. (KV Version 2) path "secret/metadata/fabio/cert/" { capabilities = ["list"] } path "secret/data/fabio/cert/*" { capabilities = ["read"] } path "test-pki/issue/fabio" { capabilities = ["update"] } ` if err := c.Sys().PutPolicy("fabio", policy); err != nil { cmd.Process.Kill() t.Fatalf("Could not create policy: %s", err) } return cmd, c } func makeToken(t *testing.T, c *vaultapi.Client, wrapTTL string, req *vaultapi.TokenCreateRequest) string { c.SetWrappingLookupFunc(func(string, string) string { return wrapTTL }) resp, err := c.Auth().Token().Create(req) if err != nil { t.Fatalf("Could not create a token: %s", err) } if wrapTTL != "" { if resp.WrapInfo == nil || resp.WrapInfo.Token == "" { t.Fatalf("Could not create a wrapped token") } return resp.WrapInfo.Token } if resp.WrapInfo != nil && resp.WrapInfo.Token != "" { t.Fatalf("Got a wrapped token but was not expecting one") } return resp.Auth.ClientToken } var vaultTestCases = []struct { desc string wrapTTL string req *vaultapi.TokenCreateRequest dropWarn bool }{ { desc: "renewable token", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", Policies: []string{"fabio"}}, }, { desc: "non-renewable token", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", Renewable: new(bool), Policies: []string{"fabio"}}, dropWarn: true, }, { desc: "renewable orphan token", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", NoParent: true, Policies: []string{"fabio"}}, }, { desc: "non-renewable orphan token", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", NoParent: true, Renewable: new(bool), Policies: []string{"fabio"}}, dropWarn: true, }, { desc: "renewable wrapped token", wrapTTL: "10s", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", Policies: []string{"fabio"}}, }, { desc: "non-renewable wrapped token", wrapTTL: "10s", req: &vaultapi.TokenCreateRequest{Lease: "1m", TTL: "1m", Renewable: new(bool), Policies: []string{"fabio"}}, dropWarn: true, }, } func TestVaultSource(t *testing.T) { const ( addr = "127.0.0.1:58421" rootToken = "token" certPath = "secret/fabio/cert" ) // start a vault server vault, client := vaultServer(t, addr, rootToken) defer vault.Process.Kill() // create a cert and store it in vault certPEM, keyPEM := makePEM("localhost", time.Minute) data := map[string]interface{}{"cert": string(certPEM), "key": string(keyPEM)} var nilSource *VaultSource // for calling helper methods mountPath, v2, err := nilSource.isKVv2(certPath, client) if err != nil { t.Fatal(err) } p := certPath + "/localhost" if v2 { t.Log("Vault: KV backend: V2") data = map[string]interface{}{ "data": data, "options": map[string]interface{}{}, } p = nilSource.addPrefixToVKVPath(p, mountPath, "data") } else { t.Log("Vault: KV backend: V1") } if _, err := client.Logical().Write(p, data); err != nil { t.Fatalf("logical.Write failed: %s", err) } pool := makeCertPool(certPEM) timeout := 500 * time.Millisecond for _, tt := range vaultTestCases { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { src := &VaultSource{ Client: &vaultClient{ addr: "http://" + addr, token: makeToken(t, client, tt.wrapTTL, tt.req), }, CertPath: certPath, } // suppress the log warning about a non-renewable token // since this is the expected behavior. dropNotRenewableWarning = tt.dropWarn testSource(t, src, pool, timeout) dropNotRenewableWarning = false }) } } func TestVaultPKISource(t *testing.T) { const ( addr = "127.0.0.1:58421" rootToken = "token" certPath = "test-pki/issue/fabio" ) // start a vault server vault, client := vaultServer(t, addr, rootToken) defer vault.Process.Kill() // mount the PKI backend err := client.Sys().Mount("test-pki", &vaultapi.MountInput{ Type: "pki", Config: vaultapi.MountConfigInput{ DefaultLeaseTTL: "1h", // default validity period of issued certificates MaxLeaseTTL: "2h", // maximum validity period of issued certificates }, }) if err != nil { t.Fatalf("Mount pki backend failed: %s", err) } // generate root CA cert resp, err := client.Logical().Write("test-pki/root/generate/internal", map[string]interface{}{ "common_name": "fabio-ca.com", "ttl": "2h", }) if err != nil { t.Fatalf("Generate root failed: %s", err) } caPool := makeCertPool([]byte(resp.Data["certificate"].(string))) // create role role := filepath.Base(certPath) _, err = client.Logical().Write("test-pki/roles/"+role, map[string]interface{}{ "allowed_domains": "", "allow_localhost": true, "allow_ip_sans": true, "organization": "Fabio Test", }) if err != nil { t.Fatalf("Write role failed: %s", err) } for _, tt := range vaultTestCases { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { src := NewVaultPKISource() src.Client = &vaultClient{ addr: "http://" + addr, token: makeToken(t, client, tt.wrapTTL, tt.req), } src.CertPath = certPath // suppress the log warning about a non-renewable token // since this is the expected behavior. dropNotRenewableWarning = tt.dropWarn testSource(t, src, caPool, 0) dropNotRenewableWarning = false }) } } // testSource runs an integration test by making an HTTPS request // to https://localhost/ expecting that the source provides a valid // certificate for "localhost". rootCAs is expected to contain a // valid root certificate or the server certificate itself so that // the HTTPS client can validate the certificate presented by the // server. func testSource(t *testing.T, source Source, rootCAs *x509.CertPool, sleep time.Duration) { const NoStrictMatch = false srvConfig, err := TLSConfig(source, NoStrictMatch, 0, 0, nil) if err != nil { t.Fatalf("TLSConfig: got %q want nil", err) } // give the source some time to initialize if necessary time.Sleep(sleep) // create an http client that will accept the root CAs // otherwise the HTTPS client will not verify the // certificate presented by the server. http11 := http11Client(rootCAs) http20, err := http20Client(rootCAs) if err != nil { t.Fatal("http20Client: ", err) } // disable log output for the next call to prevent // confusing log messages since they are expected // http: TLS handshake error from 127.0.0.1:55044: remote error: bad certificate log.SetOutput(io.Discard) defer log.SetOutput(os.Stderr) // fail calls https://localhost.org/ for which certificate validation // should fail since the hostname differs from the one in the certificate. fail := func(client *http.Client) { _, _, err := roundtrip("localhost.org", srvConfig, client) got, want := err, "x509: certificate is valid for localhost, not localhost.org" if got == nil || !strings.Contains(got.Error(), want) { t.Fatalf("got %q want %q", got, want) } } // succeed executes a roundtrip to https://localhost/ which // should return 200 OK and wantBody. succeed := func(client *http.Client, wantBody string) { code, body, err := roundtrip("localhost", srvConfig, client) if err != nil { t.Fatalf("got %v want nil", err) } if got, want := code, 200; got != want { t.Fatalf("got %v want %v", got, want) } if got, want := body, wantBody; got != want { t.Fatalf("got %v want %v", got, want) } } // make a call for which certificate validation succeeds. succeed(http11, "OK HTTP/1.1") succeed(http20, "OK HTTP/2.0") // now make the call that should fail. fail(http11) fail(http20) } // roundtrip starts a TLS server with the given server configuration and // then sends an SNI request with the given serverName. func roundtrip(serverName string, srvConfig *tls.Config, client *http.Client) (code int, body string, err error) { // create an HTTPS server and start it. It will be listening on 127.0.0.1 srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "OK ", r.Proto) })) srv.TLS = srvConfig srv.StartTLS() defer srv.Close() // configure SNI client.Transport.(*http.Transport).TLSClientConfig.ServerName = serverName // give the tls server some time to start up time.Sleep(10 * time.Millisecond) resp, err := client.Get(srv.URL) if err != nil { return 0, "", err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return 0, "", err } return resp.StatusCode, string(data), nil } // http11Client returns an HTTP client which can only // execute HTTP/1.1 requests via TLS. func http11Client(rootCAs *x509.CertPool) *http.Client { t := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: rootCAs, }, } return &http.Client{Transport: t} } // http20Client returns an HTTP client which can // execute HTTP/2.0 requests via TLS if the server // supports it. func http20Client(rootCAs *x509.CertPool) (*http.Client, error) { t := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: rootCAs, }, } if err := http2.ConfigureTransport(t); err != nil { return nil, err } return &http.Client{Transport: t}, nil } func writeFile(filename string, data []byte) { if err := os.WriteFile(filename, data, 0644); err != nil { panic(err.Error()) } } func makeCertPool(x ...[]byte) *x509.CertPool { p := x509.NewCertPool() for _, b := range x { // https://github.com/fabiolb/fabio/issues/434 if ok := p.AppendCertsFromPEM(b); !ok { panic("failed to add cert from PEM. Is the CN a DNS compatible name?") } } return p } func saveCert(dir, host string, certPEM, keyPEM []byte) (certFile, keyFile string) { certFile, keyFile = filepath.Join(dir, host+"-cert.pem"), filepath.Join(dir, host+"-key.pem") writeFile(certFile, certPEM) writeFile(keyFile, keyPEM) return certFile, keyFile } // makePEM creates a self-signed RSA certificate as two PEM blocks. // taken from crypto/tls/generate_cert.go func makePEM(host string, validFor time.Duration) (certPEM, keyPEM []byte) { const bits = 1024 priv, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { panic("Failed to generate private key: " + err.Error()) } template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Fabio Co"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(validFor), IsCA: true, DNSNames: []string{host}, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { panic("Failed to create certificate: " + err.Error()) } var cert, key bytes.Buffer pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) pem.Encode(&key, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) return cert.Bytes(), key.Bytes() } func makeCert(host string, validFor time.Duration) tls.Certificate { certPEM, keyPEM := makePEM(host, validFor) cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { panic("Failed to create certificate: " + err.Error()) } return cert } func waitFor(timeout time.Duration, up func() bool) bool { until := time.Now().Add(timeout) for { if time.Now().After(until) { return false } if up() { return true } time.Sleep(100 * time.Millisecond) } } ================================================ FILE: cert/store.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "errors" "log" "strings" "sync/atomic" ) // Store provides a dynamic certificate store which can be updated at // runtime and is safe for concurrent use. type Store struct { cs atomic.Value } // NewStore creates an empty certificate store. func NewStore() *Store { s := new(Store) s.cs.Store(certstore{}) return s } // SetCertificates replaces the certificates of the store. func (s *Store) SetCertificates(certs []tls.Certificate) { cs := certstore{Certificates: certs} cs.BuildNameToCertificate() s.cs.Store(cs) var names []string for name := range cs.NameToCertificate { names = append(names, name) } log.Printf("[INFO] cert: Store has certificates for [%q]", strings.Join(names, ",")) } func (s *Store) certstore() certstore { return s.cs.Load().(certstore) } var ErrNoCertsStored = errors.New("cert: no certificates stored") func getCertificate(cs certstore, clientHello *tls.ClientHelloInfo, strictMatch bool) (cert *tls.Certificate, err error) { if len(cs.Certificates) == 0 { return nil, ErrNoCertsStored } // There's only one choice, so no point doing any work. // However, if fallback is disabled we need to check. if !strictMatch && (len(cs.Certificates) == 1 || cs.NameToCertificate == nil) { return &cs.Certificates[0], nil } name := strings.ToLower(clientHello.ServerName) for len(name) > 0 && name[len(name)-1] == '.' { name = name[:len(name)-1] } if cert, ok := cs.NameToCertificate[name]; ok { return cert, nil } // try replacing labels in the name with wildcards until we get a match labels := strings.Split(name, ".") for i := range labels { labels[i] = "*" candidate := strings.Join(labels, ".") if cert, ok := cs.NameToCertificate[candidate]; ok { return cert, nil } } // If nothing matches, return the first certificate // unless fallback to the first cert is disabled. if strictMatch { return nil, nil } return &cs.Certificates[0], nil } type certstore struct { NameToCertificate map[string]*tls.Certificate Certificates []tls.Certificate } // BuildNameToCertificate parses Certificates and builds NameToCertificate // from the CommonName and SubjectAlternateName fields of each of the leaf // certificates. func (c *certstore) BuildNameToCertificate() { c.NameToCertificate = make(map[string]*tls.Certificate) for i := range c.Certificates { cert := &c.Certificates[i] x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) if err != nil { continue } if len(x509Cert.Subject.CommonName) > 0 { c.NameToCertificate[x509Cert.Subject.CommonName] = cert } for _, san := range x509Cert.DNSNames { c.NameToCertificate[san] = cert } } } ================================================ FILE: cert/store_test.go ================================================ package cert import ( "crypto/tls" "errors" "reflect" "testing" "time" ) func TestGetCertificate(t *testing.T) { fooCert := makeCert("foo.com", time.Minute) barCert := makeCert("bar.com", time.Minute) wildBarCert := makeCert("*.bar.com", time.Minute) tests := []struct { desc string certs []tls.Certificate hello *tls.ClientHelloInfo strict bool cert *tls.Certificate err error }{ // edge cases { desc: "no certs", certs: nil, hello: &tls.ClientHelloInfo{ServerName: "foo.com"}, cert: nil, err: errors.New("cert: no certificates stored"), }, { desc: "server name ends in dot", certs: []tls.Certificate{fooCert, barCert}, hello: &tls.ClientHelloInfo{ServerName: "bar.com."}, cert: &barCert, err: nil, }, // happy flows { desc: "one cert exact match", certs: []tls.Certificate{fooCert}, hello: &tls.ClientHelloInfo{ServerName: "foo.com"}, cert: &fooCert, err: nil, }, { desc: "one cert fallback", certs: []tls.Certificate{fooCert}, hello: &tls.ClientHelloInfo{ServerName: "bar.com"}, cert: &fooCert, err: nil, }, { desc: "one cert strict match", certs: []tls.Certificate{fooCert}, hello: &tls.ClientHelloInfo{ServerName: "bar.com"}, cert: nil, strict: true, err: nil, }, { desc: "two certs exact match", certs: []tls.Certificate{fooCert, barCert}, hello: &tls.ClientHelloInfo{ServerName: "bar.com"}, cert: &barCert, err: nil, }, { desc: "two certs fallback", certs: []tls.Certificate{fooCert, barCert}, hello: &tls.ClientHelloInfo{ServerName: "whiz.com"}, cert: &fooCert, err: nil, }, { desc: "two certs strict match", certs: []tls.Certificate{fooCert, barCert}, hello: &tls.ClientHelloInfo{ServerName: "whiz.com"}, cert: nil, strict: true, err: nil, }, { desc: "wildcard cert", certs: []tls.Certificate{fooCert, wildBarCert}, hello: &tls.ClientHelloInfo{ServerName: "quux.bar.com"}, cert: &wildBarCert, err: nil, }, { desc: "wildcard cert strict match", certs: []tls.Certificate{fooCert, wildBarCert}, hello: &tls.ClientHelloInfo{ServerName: "quux.bar.com"}, cert: &wildBarCert, strict: true, err: nil, }, } for i, tt := range tests { cs := certstore{Certificates: tt.certs} cs.BuildNameToCertificate() cert, err := getCertificate(cs, tt.hello, tt.strict) if got, want := err, tt.err; !reflect.DeepEqual(got, want) { t.Errorf("%d: %q: got %v want %v", i, tt.desc, got, want) continue } if got, want := cert, tt.cert; !reflect.DeepEqual(got, want) { t.Errorf("%d: %q: got %+v want %+v", i, tt.desc, got, want) } } } ================================================ FILE: cert/vault_client.go ================================================ package cert import ( "encoding/json" "errors" "log" "os" "strings" "sync" "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/consts" ) // vaultClient wraps an *api.Client and takes care of token renewal // automatically. type vaultClient struct { client *api.Client addr string // overrides the default config token string // overrides the VAULT_TOKEN environment variable fetchVaultToken string prevFetchedToken string mu sync.Mutex } func NewVaultClient(fetchVaultToken string) *vaultClient { return &vaultClient{ fetchVaultToken: fetchVaultToken, } } var DefaultVaultClient = &vaultClient{} func (c *vaultClient) Get() (*api.Client, error) { c.mu.Lock() defer c.mu.Unlock() if c.client != nil { if c.fetchVaultToken != "" { token := strings.TrimSpace(getVaultToken(c.fetchVaultToken)) if token != c.prevFetchedToken { log.Printf("[DEBUG] vault: token has changed, setting new token") // did we get a wrapped token? resp, err := c.client.Logical().Unwrap(token) switch { case err == nil: log.Printf("[INFO] vault: Unwrapped token %s", token) c.client.SetToken(resp.Auth.ClientToken) case strings.HasPrefix(err.Error(), "no value found at"): // not a wrapped token default: return nil, err } c.prevFetchedToken = token } } return c.client, nil } conf := api.DefaultConfig() if err := conf.ReadEnvironment(); err != nil { return nil, err } if c.addr != "" { conf.Address = c.addr } client, err := api.NewClient(conf) if err != nil { return nil, err } if c.fetchVaultToken != "" { token := strings.TrimSpace(getVaultToken(c.fetchVaultToken)) log.Printf("[DEBUG] vault: fetching initial token") if token != c.prevFetchedToken { c.token = token c.prevFetchedToken = token } } if c.token != "" { client.SetToken(c.token) } token := client.Token() if token == "" { return nil, errors.New("vault: no token") } // did we get a wrapped token? resp, err := client.Logical().Unwrap(token) var respErr *api.ResponseError contains := func(haystack []string, needle string) bool { for _, h := range haystack { if h == needle { return true } } return false } switch { case err == nil: log.Printf("[INFO] vault: Unwrapped token %s", token) client.SetToken(resp.Auth.ClientToken) case errors.As(err, &respErr) && contains(respErr.Errors, consts.ErrInvalidWrappingToken.Error()): // not wrapped default: return nil, err } c.client = client go c.keepTokenAlive() return client, nil } // dropNotRenewableWarning controls whether the 'Token is not renewable' // warning is logged. This is useful for testing where this is the expected // behavior. On production, this should always be set to false. var dropNotRenewableWarning bool func (c *vaultClient) keepTokenAlive() { resp, err := c.client.Auth().Token().LookupSelf() if err != nil { log.Printf("[WARN] vault: lookup-self failed, token renewal is disabled: %s", err) return } b, _ := json.Marshal(resp.Data) var data struct { ExpireTime time.Time `json:"expire_time"` TTL int `json:"ttl"` CreationTTL int `json:"creation_ttl"` Renewable bool `json:"renewable"` } if err := json.Unmarshal(b, &data); err != nil { log.Printf("[WARN] vault: lookup-self failed, token renewal is disabled: %s", err) return } switch { case data.Renewable: // no-op case data.ExpireTime.IsZero(): // token doesn't expire return case dropNotRenewableWarning: return default: ttl := time.Until(data.ExpireTime) ttl = ttl.Round(time.Second) log.Printf("[WARN] vault: Token is not renewable and will expire %s from now at %s", ttl, data.ExpireTime.Format(time.RFC3339)) return } ttl := time.Duration(data.TTL) * time.Second timer := time.NewTimer(ttl / 2) for range timer.C { resp, err := c.client.Auth().Token().RenewSelf(data.CreationTTL) if err != nil { log.Printf("[WARN] vault: Failed to renew token: %s", err) timer.Reset(time.Second) // TODO: backoff? abort after N consecutive failures? continue } if !resp.Auth.Renewable || resp.Auth.LeaseDuration == 0 { // token isn't renewable anymore, we're done. return } ttl = time.Duration(resp.Auth.LeaseDuration) * time.Second timer.Reset(ttl / 2) } } func getVaultToken(c string) string { var token string c = strings.TrimSpace(c) cArray := strings.SplitN(c, ":", 2) if len(cArray) < 2 { log.Printf("[WARN] vault: vaultfetchtoken not properly set") return token } switch cArray[0] { case "file": b, err := os.ReadFile(cArray[1]) // just pass the file name if err != nil { log.Printf("[WARN] vault: Failed to fetch token from %s", c) } else { token = string(b) log.Printf("[DEBUG] vault: Successfully fetched token from %s", c) return token } case "env": token = os.Getenv(cArray[1]) if len(token) == 0 { log.Printf("[WARN] vault: Failed to fetch token from %s", c) } else { log.Printf("[DEBUG] vault: Successfully fetched token from %s", c) return token } default: log.Printf("[WARN] vault: vaultfetchtoken not properly set") } return token } ================================================ FILE: cert/vault_pki_source.go ================================================ package cert import ( "bytes" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "log" "math/big" "sync" "time" ) // VaultPKISource implements a certificate source which issues TLS certificates // on-demand using a Vault PKI backend. Client authorization certificates are // loaded from a generic backend (same as in VaultSource). The Vault token // should be set through the VAULT_TOKEN environment variable. // // The TLS certificates are re-issued automatically before they expire. type VaultPKISource struct { Client *vaultClient certsCh chan []tls.Certificate certs map[string]tls.Certificate // issued certs CertPath string ClientCAPath string CAUpgradeCN string // Re-issue certificates this long before they expire. Cannot be less then // one hour. Refresh time.Duration mu sync.Mutex } func NewVaultPKISource() *VaultPKISource { return &VaultPKISource{ certs: make(map[string]tls.Certificate, 0), certsCh: make(chan []tls.Certificate, 1), } } func (s *VaultPKISource) LoadClientCAs() (*x509.CertPool, error) { return (&VaultSource{ Client: s.Client, ClientCAPath: s.ClientCAPath, CAUpgradeCN: s.CAUpgradeCN, }).LoadClientCAs() } func (s *VaultPKISource) Certificates() chan []tls.Certificate { return s.certsCh } func (s *VaultPKISource) Issue(commonName string) (*tls.Certificate, error) { c, err := s.Client.Get() if err != nil { return nil, fmt.Errorf("vault: client: %s", err) } resp, err := c.Logical().Write(s.CertPath, map[string]interface{}{ "common_name": commonName, }) if err != nil { fmt.Printf("Issue: %v\n", err) return nil, fmt.Errorf("vault: issue: %s", err) } b, _ := json.Marshal(resp.Data) var data struct { PrivateKey string `json:"private_key"` Certificate string `json:"certificate"` CAChain []string `json:"ca_chain"` } if err := json.Unmarshal(b, &data); err != nil { return nil, fmt.Errorf("vault: issue: %s", err) } if data.PrivateKey == "" { return nil, fmt.Errorf("vault: issue: missing private key") } if data.Certificate == "" { return nil, fmt.Errorf("vault: issue: missing certificate") } key := []byte(data.PrivateKey) fullChain := []byte(data.Certificate) for _, c := range data.CAChain { fullChain = append(fullChain, '\n') fullChain = append(fullChain, []byte(c)...) } cert, err := tls.X509KeyPair(fullChain, key) if err != nil { return nil, fmt.Errorf("vault: issue: %s", err) } x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) if err != nil { // Should never happen because x509.ParseCertificate did this // successfully already, but threw the result away. return nil, fmt.Errorf("vault: issue: %s", err) } refresh := s.Refresh if refresh < time.Hour { refresh = time.Hour } expires := x509Cert.NotAfter certTTL := time.Until(expires) - refresh time.AfterFunc(certTTL, func() { _, err := s.Issue(commonName) if err != nil { log.Printf("[ERROR] cert: vault: Failed to re-issue cert for %s: %s", commonName, err) // TODO: Now what? Retry? Do nothing? return } }) s.mu.Lock() s.certs[commonName] = cert allCerts := make([]tls.Certificate, 0, len(s.certs)) for _, c := range s.certs { allCerts = append(allCerts, c) } s.mu.Unlock() go func() { s.certsCh <- allCerts }() log.Printf("[INFO] cert: vault: issued cert for %s; serial = %s", commonName, s.formatSerial(x509Cert.SerialNumber)) return &cert, nil } func (*VaultPKISource) formatSerial(sn *big.Int) string { var buf bytes.Buffer for _, b := range sn.Bytes() { if buf.Len() > 0 { buf.WriteByte('-') } fmt.Fprintf(&buf, "%02x", b) } return buf.String() } ================================================ FILE: cert/vault_source.go ================================================ package cert import ( "crypto/tls" "crypto/x509" "fmt" "log" "path" "strings" "time" "github.com/hashicorp/vault/api" ) // VaultSource implements a certificate source which loads // TLS and client authorization certificates from a Vault server. // The Vault token should be set through the VAULT_TOKEN environment // variable. // // The TLS certificates are updated automatically when Refresh // is not zero. Refresh cannot be less than one second to prevent // busy loops. type VaultSource struct { Client *vaultClient CertPath string ClientCAPath string CAUpgradeCN string Refresh time.Duration } func (s *VaultSource) LoadClientCAs() (*x509.CertPool, error) { if s.ClientCAPath == "" { return nil, nil } return newCertPool(s.ClientCAPath, s.CAUpgradeCN, s.load) } func (s *VaultSource) Certificates() chan []tls.Certificate { ch := make(chan []tls.Certificate, 1) go watch(ch, s.Refresh, s.CertPath, s.load) return ch } func (s *VaultSource) load(path string) (pemBlocks map[string][]byte, err error) { pemBlocks = map[string][]byte{} // get will read a key=value pair from the secret // and store it as -{cert,key}.pem so that // they are recognized by the post-processing function // which assembles the certificates. // The value can be stored either as string or []byte. get := func(name, typ string, secret *api.Secret, v2 bool) { data := secret.Data if v2 { x, ok := secret.Data["data"] if !ok { return } data, ok = x.(map[string]interface{}) if !ok { return } } v := data[typ] if v == nil { return } var b []byte switch v := v.(type) { case string: b = []byte(v) case []byte: b = v default: log.Printf("[WARN] cert: key %s has type %T", name, v) return } pemBlocks[name+"-"+typ+".pem"] = b } c, err := s.Client.Get() if err != nil { return nil, fmt.Errorf("vault: client: %s", err) } mountPath, v2, err := s.isKVv2(path, c) if err != nil { return nil, fmt.Errorf("vault: query mount path: %s", err) } // get the subkeys under 'path'. // Each subkey refers to a certificate. p := path if v2 { p = s.addPrefixToVKVPath(p, mountPath, "metadata") } certs, err := c.Logical().List(p) if err != nil { return nil, fmt.Errorf("vault: list: %s", err) } if certs == nil || certs.Data["keys"] == nil { return nil, nil } for _, x := range certs.Data["keys"].([]interface{}) { name := x.(string) p := path + "/" + name if v2 { p = s.addPrefixToVKVPath(p, mountPath, "data") } secret, err := c.Logical().Read(p) if err != nil { log.Printf("[WARN] cert: Failed to read %s from Vault: %s", p, err) continue } if secret == nil { log.Printf("[WARN] cert: Failed to find %s in Vault: %s", p, err) continue } get(name, "cert", secret, v2) get(name, "key", secret, v2) } return pemBlocks, nil } func (s *VaultSource) addPrefixToVKVPath(p, mountPath, apiPrefix string) string { p = strings.TrimPrefix(p, mountPath) return path.Join(mountPath, apiPrefix, p) } func (s *VaultSource) isKVv2(path string, client *api.Client) (string, bool, error) { mountPath, version, err := s.kvPreflightVersionRequest(client, path) if err != nil { return "", false, err } return mountPath, version == 2, nil } func (s *VaultSource) kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { resp, err := client.Logical().ReadRaw("sys/internal/ui/mounts/" + path) if resp != nil { defer resp.Body.Close() } if err != nil { // If we get a 404 we are using an older version of vault, default to // version 1 if resp != nil && resp.StatusCode == 404 { return "", 1, nil } return "", 0, err } secret, err := api.ParseSecret(resp.Body) if err != nil { return "", 0, err } var mountPath string if mountPathRaw, ok := secret.Data["path"]; ok { mountPath = mountPathRaw.(string) } options := secret.Data["options"] if options == nil { return mountPath, 1, nil } versionRaw := options.(map[string]interface{})["version"] if versionRaw == nil { return mountPath, 1, nil } version := versionRaw.(string) switch version { case "", "1": return mountPath, 1, nil case "2": return mountPath, 2, nil } return mountPath, 1, nil } ================================================ FILE: cert/watch.go ================================================ package cert import ( "crypto/tls" "log" "reflect" "time" ) // watch monitors the result of the loadFn function for changes. func watch(ch chan []tls.Certificate, refresh time.Duration, path string, loadFn func(path string) (map[string][]byte, error)) { once := refresh <= 0 // do not refresh more often than once a second to prevent busy loops if refresh < time.Second { refresh = time.Second } var last map[string][]byte for { next, err := loadFn(path) if err != nil { log.Printf("[ERROR] cert: Cannot load certificates from %s. %s", path, err) time.Sleep(refresh) continue } if reflect.DeepEqual(next, last) { time.Sleep(refresh) continue } certs, err := loadCertificates(next) if err != nil { log.Printf("[ERROR] cert: Cannot make certificates: %s", err) continue } ch <- certs last = next if once { return } } } ================================================ FILE: config/config.go ================================================ package config import ( "net/http" "regexp" "time" ) type Config struct { Log Log ProfileMode string ProfilePath string Listen []Listen Metrics Metrics BGP BGP UI UI Registry Registry Proxy Proxy Runtime Runtime GlobCacheSize int Insecure bool GlobMatchingDisabled bool } type CertSource struct { Header http.Header Name string Type string CertPath string KeyPath string ClientCAPath string CAUpgradeCN string VaultFetchToken string Refresh time.Duration } type Listen struct { CertSource CertSource Addr string Proto string TLSCiphers []uint16 ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration ProxyHeaderTimeout time.Duration Refresh time.Duration TLSMinVersion uint16 TLSMaxVersion uint16 StrictMatch bool ProxyProto bool } type Source struct { Scheme string Host string Port string LinkEnabled bool NewTab bool } type RoutingTable struct { Source Source } type UI struct { RoutingTable RoutingTable Color string Title string Access string Listen Listen } type Proxy struct { GZIPContentTypes *regexp.Regexp AuthSchemes map[string]AuthScheme Strategy string Matcher string LocalIP string ClientIPHeader string TLSHeader string TLSHeaderValue string RequestID string STSHeader STSHeader NoRouteStatus int MaxConn int ShutdownWait time.Duration DeregisterGracePeriod time.Duration DialTimeout time.Duration ResponseHeaderTimeout time.Duration KeepAliveTimeout time.Duration IdleConnTimeout time.Duration FlushInterval time.Duration GlobalFlushInterval time.Duration GRPCMaxRxMsgSize int GRPCMaxTxMsgSize int GRPCGShutdownTimeout time.Duration } type STSHeader struct { MaxAge int Subdomains bool Preload bool } type Runtime struct { GOGC int GOMAXPROCS int } type Circonus struct { APIKey string APIApp string APIURL string CheckID string BrokerID string SubmissionURL string } type Prometheus struct { Subsystem string Path string Buckets []float64 } type Log struct { AccessFormat string AccessTarget string RoutesFormat string Level string } type Metrics struct { Circonus Circonus Target string Prefix string Names string GraphiteAddr string StatsDAddr string DogstatsdAddr string Prometheus Prometheus Interval time.Duration Timeout time.Duration Retry time.Duration } type Registry struct { Static Static File File Backend string Custom Custom Consul Consul Timeout time.Duration Retry time.Duration } type Static struct { NoRouteHTML string Routes string } type File struct { NoRouteHTMLPath string RoutesPath string } type Consul struct { Addr string Scheme string Token string KVPath string NoRouteHTMLPath string TagPrefix string Namespace string ServiceAddr string ServiceName string CheckScheme string ChecksRequired string TLS ConsulTlS ServiceTags []string ServiceStatus []string CheckInterval time.Duration CheckTimeout time.Duration ServiceMonitors int PollInterval time.Duration Register bool CheckTLSSkipVerify bool RequireConsistent bool AllowStale bool } type Custom struct { Host string Path string QueryParams string Scheme string NoRouteHTML string PollInterval time.Duration Timeout time.Duration CheckTLSSkipVerify bool } type AuthScheme struct { Name string Type string Basic BasicAuth } type BasicAuth struct { ModTime time.Time // the htpasswd file last modification time Realm string File string Refresh time.Duration } type ConsulTlS struct { KeyFile string CertFile string CAFile string CAPath string InsecureSkipVerify bool } type BGP struct { RouterID string GRPCListenAddress string CertFile string KeyFile string GOBGPDCfgFile string NextHop string AnycastAddresses []string ListenAddresses []string Peers []BGPPeer Asn uint ListenPort int BGPEnabled bool EnableGRPC bool GRPCTLS bool } type BGPPeer struct { NeighborAddress string Password string NeighborPort uint Asn uint MultiHopLength uint MultiHop bool } ================================================ FILE: config/default.go ================================================ package config import ( "os" "runtime" "time" ) var defaultValues = struct { ListenerValue string CertSourcesValue string AuthSchemesValue string UIListenerValue string GZIPContentTypesValue string BGPPeersValue string ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration }{ ListenerValue: ":9999", UIListenerValue: ":9998", } var defaultConfig = &Config{ ProfilePath: os.TempDir(), Log: Log{ AccessFormat: "common", RoutesFormat: "delta", Level: "INFO", }, Metrics: Metrics{ Prefix: "{{clean .Hostname}}.{{clean .Exec}}", Names: "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}", Interval: 30 * time.Second, Timeout: 10 * time.Second, Retry: 500 * time.Millisecond, Circonus: Circonus{ APIApp: "fabio", }, Prometheus: Prometheus{ Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, Path: "/metrics", }, }, Proxy: Proxy{ MaxConn: 10000, Strategy: "rnd", Matcher: "prefix", NoRouteStatus: 404, DialTimeout: 30 * time.Second, FlushInterval: time.Second, GlobalFlushInterval: 0, LocalIP: LocalIPString(), AuthSchemes: map[string]AuthScheme{}, IdleConnTimeout: 15 * time.Second, GRPCMaxRxMsgSize: 4 * 1024 * 1024, // 4M GRPCMaxTxMsgSize: 4 * 1024 * 1024, // 4M GRPCGShutdownTimeout: time.Second * 2, }, Registry: Registry{ Backend: "consul", Consul: Consul{ Addr: "localhost:8500", Scheme: "http", KVPath: "/fabio/config", NoRouteHTMLPath: "/fabio/noroute.html", TagPrefix: "urlprefix-", Register: true, Namespace: "", ServiceAddr: ":9998", ServiceName: "fabio", ServiceStatus: []string{"passing"}, ServiceMonitors: 1, CheckInterval: time.Second, CheckTimeout: 3 * time.Second, CheckScheme: "http", ChecksRequired: "one", PollInterval: 0, RequireConsistent: true, AllowStale: false, }, Custom: Custom{ Host: "", Scheme: "https", CheckTLSSkipVerify: false, PollInterval: 5, NoRouteHTML: "", Timeout: 10, Path: "", QueryParams: "", }, Timeout: 10 * time.Second, Retry: 500 * time.Millisecond, }, Runtime: Runtime{ GOGC: 100, GOMAXPROCS: runtime.NumCPU(), }, UI: UI{ Listen: Listen{ Addr: ":9998", Proto: "http", }, Color: "light-green", Access: "rw", RoutingTable: RoutingTable{ Source: Source{ LinkEnabled: false, NewTab: true, Scheme: "http", }, }, }, GlobCacheSize: 1000, BGP: BGP{ BGPEnabled: false, Asn: 65000, AnycastAddresses: nil, RouterID: "", ListenPort: 179, ListenAddresses: []string{"0.0.0.0"}, Peers: nil, EnableGRPC: false, GRPCListenAddress: "127.0.0.1:50051", }, } var defaultBGPPeer = &BGPPeer{ MultiHopLength: 2, } ================================================ FILE: config/flagset.go ================================================ package config import ( "flag" "fmt" "strconv" "strings" "github.com/magiconair/properties" ) // -- stringSliceValue type stringSliceValue []string func newStringSliceValue(val []string, p *[]string) *stringSliceValue { *p = val return (*stringSliceValue)(p) } func (v *stringSliceValue) Set(s string) error { *v = []string{} for _, x := range strings.Split(s, ",") { x = strings.TrimSpace(x) if x == "" { continue } *v = append(*v, x) } return nil } func (v *stringSliceValue) Get() interface{} { return []string(*v) } func (v *stringSliceValue) String() string { return strings.Join(*v, ",") } type floatSliceValue []float64 func newFloatSliceValue(val []float64, p *[]float64) *floatSliceValue { *p = val return (*floatSliceValue)(p) } func (f *floatSliceValue) String() string { strs := make([]string, len(*f)) for i, v := range *f { strs[i] = strconv.FormatFloat(v, 'f', -1, 64) } return strings.Join(strs, ",") } func (f *floatSliceValue) Set(s string) error { *f = []float64{} for _, x := range strings.Split(s, ",") { x = strings.TrimSpace(x) if x == "" { continue } v, err := strconv.ParseFloat(x, 64) if err != nil { return fmt.Errorf("error parsing float slice value %s: %w", x, err) } *f = append(*f, v) } return nil } // -- FlagSet type FlagSet struct { flag.FlagSet set map[string]bool } func NewFlagSet(name string, errorHandling flag.ErrorHandling) *FlagSet { fs := &FlagSet{set: make(map[string]bool)} fs.Init(name, errorHandling) return fs } // IsSet returns true if a variable was set via any mechanism. func (f *FlagSet) IsSet(name string) bool { return f.set[name] } func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) { f.Var(newStringSliceValue(value, p), name, usage) } func (f *FlagSet) FloatSliceVar(p *[]float64, name string, value []float64, usage string) { f.Var(newFloatSliceValue(value, p), name, usage) } // ParseFlags parses command line arguments and provides fallback // values from environment variables and config file values. // Environment variables are case-insensitive and can have either // of the provided prefixes. func (f *FlagSet) ParseFlags(args, environ, prefixes []string, p *properties.Properties) error { if err := f.Parse(args); err != nil { return err } if len(prefixes) == 0 { prefixes = []string{""} } // parse environment in case-insensitive way env := map[string]string{} for _, e := range environ { p := strings.SplitN(e, "=", 2) env[strings.ToUpper(p[0])] = p[1] } // determine all values that were set via cmdline f.Visit(func(fl *flag.Flag) { f.set[fl.Name] = true }) // lookup the rest via environ and properties f.VisitAll(func(fl *flag.Flag) { // skip if already set if f.set[fl.Name] { return } // check environment variables for _, pfx := range prefixes { name := strings.ToUpper(pfx + strings.ReplaceAll(fl.Name, ".", "_")) if val, ok := env[name]; ok { f.set[fl.Name] = true f.Set(fl.Name, val) return } } // check properties if p == nil { return } if val, ok := p.Get(fl.Name); ok { f.set[fl.Name] = true f.Set(fl.Name, val) return } }) return nil } ================================================ FILE: config/flagset_test.go ================================================ package config import ( "flag" "reflect" "testing" "github.com/magiconair/properties" ) func TestParseFlags(t *testing.T) { props := func(s string) *properties.Properties { return properties.MustLoadString(s) } tests := []struct { desc string args []string env []string prefix []string props string a []string kv map[string]string kvs []map[string]string v string }{ { desc: "cmdline should win", args: []string{"-v", "cmdline"}, env: []string{"v=env"}, props: "v=props", v: "cmdline", }, { desc: "env should win", env: []string{"v=env"}, props: "v=props", v: "env", }, { desc: "env with prefix should win", env: []string{"v=env", "p_v=prefix"}, prefix: []string{"p_"}, props: "v=props", v: "prefix", }, { desc: "props should win", props: "v=props", v: "props", }, { desc: "string slice in cmdline", args: []string{"-a", "1,2,3"}, a: []string{"1", "2", "3"}, }, { desc: "string slice in env", env: []string{"a=1,2,3"}, a: []string{"1", "2", "3"}, }, { desc: "string slice in props", props: "a=1,2,3", a: []string{"1", "2", "3"}, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { var a []string var v string f := NewFlagSet("test", flag.ExitOnError) f.StringVar(&v, "v", "", "") f.StringSliceVar(&a, "a", nil, "") err := f.ParseFlags(tt.args, tt.env, tt.prefix, props(tt.props)) if err != nil { t.Errorf("got %v want nil", err) } if got, want := v, tt.v; got != want { t.Errorf("got %q want %q", got, want) } if got, want := a, tt.a; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } } func TestDefaults(t *testing.T) { a, aDefault := []string{}, []string{"x"} v, vDefault := "", "x" f := NewFlagSet("test", flag.ExitOnError) f.StringVar(&v, "v", vDefault, "") f.StringSliceVar(&a, "a", aDefault, "") if got, want := v, vDefault; got != want { t.Errorf("got %v want %v", got, want) } if got, want := a, aDefault; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } ================================================ FILE: config/kvslice.go ================================================ package config import ( "errors" "strconv" "strings" ) // parseKVSlice parses a configuration string in the form // // key=val;key=val,key=val;key=val // // into a list of string maps. maps are separated by comma and key/value // pairs within a map are separated by semicolons. The first key/value // pair of a map can omit the key and its value will be stored under the // empty key. This allows support of legacy configuration formats which // are // // val;opt1=val1;opt2=val2;... func parseKVSlice(in string) ([]map[string]string, error) { var keyOrFirstVal string maps := []map[string]string{} m := map[string]string{} newMap := func() { if len(m) > 0 { maps = append(maps, m) m = map[string]string{} } } v := "" s := []rune(in) state := stateFirstKey for len(s) > 0 { typ, val, n := lex(s) s = s[n:] // fmt.Println("parse:", "typ:", typ, "val:", val, "v:", v, "state:", string(state), "s:", string(s)) switch state { case stateFirstKey: switch typ { case itemText: keyOrFirstVal = strings.TrimSpace(val) state = stateAfterFirstKey case itemComma, itemSemicolon: continue default: return nil, errors.New(val) } // the first value is allowed to omit the key // a=b;c=d and b;c=d are valid case stateAfterFirstKey: switch typ { case itemEqual: state = stateVal case itemComma: if keyOrFirstVal != "" { m[""] = keyOrFirstVal } newMap() state = stateFirstKey case itemSemicolon: if keyOrFirstVal != "" { m[""] = keyOrFirstVal } state = stateKey default: return nil, errors.New(val) } case stateKey: switch typ { case itemText: keyOrFirstVal = strings.TrimSpace(val) state = stateEqual case itemComma, itemSemicolon: continue default: return nil, errors.New(val) } case stateEqual: switch typ { case itemEqual: state = stateVal default: return nil, errors.New(val) } case stateVal: switch typ { case itemText, itemEqual: v += val case itemComma: m[keyOrFirstVal] = v v = "" newMap() state = stateFirstKey case itemSemicolon: m[keyOrFirstVal] = v v = "" state = stateKey default: return nil, errors.New(val) } } } switch state { case stateVal: m[keyOrFirstVal] = v case stateAfterFirstKey: if keyOrFirstVal != "" { m[""] = keyOrFirstVal } } if len(m) > 0 { maps = append(maps, m) } if len(maps) == 0 { return nil, nil } return maps, nil } type itemType string const ( itemText itemType = "TEXT" itemEqual itemType = "EQUAL" itemSemicolon itemType = "SEMICOLON" itemComma itemType = "COMMA" itemError itemType = "ERROR" ) func (t itemType) String() string { return string(t) } type state string const ( // lexer states stateStart state = "start" stateText state = "text" stateQText state = "qtext" stateQTextEnd state = "qtextend" stateQTextEsc state = "qtextesc" // parser states stateFirstKey state = "first-key" stateKey state = "key" stateEqual state = "equal" stateVal state = "val" stateAfterFirstKey state = "equal-comma-semicolon" ) func lex(s []rune) (itemType, string, int) { isComma := func(r rune) bool { return r == ',' } isSemicolon := func(r rune) bool { return r == ';' } isEqual := func(r rune) bool { return r == '=' } isEscape := func(r rune) bool { return r == '\\' } isQuote := func(r rune) bool { return r == '"' || r == '\'' } var quote rune state := stateStart for i, r := range s { // fmt.Println("lex:", "i:", i, "r:", string(r), "state:", string(state)) switch state { case stateStart: switch { case isComma(r): return itemComma, string(r), 1 case isSemicolon(r): return itemSemicolon, string(r), 1 case isEqual(r): return itemEqual, string(r), 1 case isQuote(r): quote = r state = stateQText default: state = stateText } case stateText: switch { case isComma(r) || isSemicolon(r) || isEqual(r): return itemText, string(s[:i]), i default: // state = stateText } case stateQText: switch { case r == quote: state = stateQTextEnd case isEscape(r): state = stateQTextEsc default: // state = stateQText } case stateQTextEsc: state = stateQText case stateQTextEnd: v, err := strconv.Unquote(string(s[:i])) if err != nil { return itemError, "invalid escape sequence", i } return itemText, v, i } } // fmt.Println("lex:", "state:", string(state)) switch state { case stateQText: return itemError, "unbalanced quotes", len(s) case stateQTextEsc: return itemError, "unterminated escape sequence", len(s) case stateQTextEnd: v, err := strconv.Unquote(string(s)) if err != nil { return itemError, "invalid escape sequence", len(s) } return itemText, v, len(s) default: return itemText, string(s), len(s) } } ================================================ FILE: config/kvslice_test.go ================================================ package config import ( "reflect" "testing" ) func TestParseKVSlice(t *testing.T) { tests := []struct { desc string s string m []map[string]string err error }{ {"empty", "", nil, nil}, {"key=val", "a=b", []map[string]string{{"a": "b"}}, nil}, {"key with spaces", " a =b", []map[string]string{{"a": "b"}}, nil}, {"quoted value", "a=\"b\"", []map[string]string{{"a": "b"}}, nil}, {"single quoted value", "a='b'", []map[string]string{{"a": "b"}}, nil}, {"quoted value with backslash", `a="b\\\""`, []map[string]string{{"a": `b\"`}}, nil}, {"ignore empty map front", ",a=b", []map[string]string{{"a": "b"}}, nil}, {"ignore empty map back", "a=b,", []map[string]string{{"a": "b"}}, nil}, {"ignore empty value front", ";a=b", []map[string]string{{"a": "b"}}, nil}, {"ignore empty value back", "a=b;", []map[string]string{{"a": "b"}}, nil}, {"multiple values", "a=b;c=d", []map[string]string{{"a": "b", "c": "d"}}, nil}, {"multiple maps", "a=b,c=d", []map[string]string{{"a": "b"}, {"c": "d"}}, nil}, {"multiple values and maps", "a=b;c=d,e=f;g=h", []map[string]string{{"a": "b", "c": "d"}, {"e": "f", "g": "h"}}, nil}, {"first key empty", "b", []map[string]string{{"": "b"}}, nil}, {"first key empty and more values", "b;c=d", []map[string]string{{"": "b", "c": "d"}}, nil}, {"first key empty and more maps", "b,c", []map[string]string{{"": "b"}, {"": "c"}}, nil}, {"first key empty and more maps and values", "b;c=d,e;f=g", []map[string]string{{"": "b", "c": "d"}, {"": "e", "f": "g"}}, nil}, {"issue 305", "a=b=c,d=e=f", []map[string]string{{"a": "b=c"}, {"d": "e=f"}}, nil}, {"issue 305", "a=b=c;d=e=f", []map[string]string{{"a": "b=c", "d": "e=f"}}, nil}, {"issue 305", "a=b;d=e=f", []map[string]string{{"a": "b", "d": "e=f"}}, nil}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { m, err := parseKVSlice(tt.s) if got, want := err, tt.err; !reflect.DeepEqual(got, want) { t.Fatalf("got error %v want %v", got, want) } if got, want := m, tt.m; !reflect.DeepEqual(got, want) { t.Fatalf("got %#v want %#v", got, want) } }) } } ================================================ FILE: config/load.go ================================================ package config import ( "crypto/tls" "errors" "flag" "fmt" "log" "net/http" "regexp" "runtime" "strconv" "strings" "time" gs "github.com/hashicorp/go-sockaddr/template" "github.com/magiconair/properties" ) var tlsciphers map[string]uint16 func loadCiphers() { tlsciphers = make(map[string]uint16) for _, c := range tls.CipherSuites() { tlsciphers[c.Name] = c.ID } for _, c := range tls.InsecureCipherSuites() { tlsciphers[c.Name] = c.ID } } func Load(args, environ []string) (cfg *Config, err error) { var props *properties.Properties loadCiphers() cmdline, path, version, err := parse(args) switch { case err != nil: return nil, err case version: return nil, nil case path != "": switch { case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"): props, err = properties.LoadURL(path) case path != "": props, err = properties.LoadFile(path, properties.UTF8) } if err != nil { return nil, err } } envprefix := []string{"FABIO_", ""} return load(cmdline, environ, envprefix, props) } var errInvalidConfig = errors.New("invalid or missing path to config file") // parse extracts the version and config file flags from the command // line arguments and returns the individual parts. Test flags are // ignored. func parse(args []string) (cmdline []string, path string, version bool, err error) { if len(args) < 1 { panic("missing exec name") } // always copy the name of the executable cmdline = args[:1] // parse rest of the arguments for i := 1; i < len(args); i++ { arg := args[i] switch { // version flag case arg == "-v" || arg == "-version" || arg == "--version": return nil, "", true, nil // config file without '=' case arg == "-cfg" || arg == "--cfg": if i >= len(args)-1 { return nil, "", false, errInvalidConfig } path = args[i+1] i++ // config file with '='. needs unquoting case strings.HasPrefix(arg, "-cfg=") || strings.HasPrefix(arg, "--cfg="): if strings.HasPrefix(arg, "-cfg=") { path = arg[len("-cfg="):] } else { path = arg[len("--cfg="):] } switch { case path == "": return nil, "", false, errInvalidConfig case path[0] == '\'': path = strings.Trim(path, "'") case path[0] == '"': path = strings.Trim(path, "\"") } if path == "" { return nil, "", false, errInvalidConfig } // ignore test flags case strings.HasPrefix(arg, "-test."): continue default: cmdline = append(cmdline, arg) } } return cmdline, path, false, nil } func load(cmdline, environ, envprefix []string, props *properties.Properties) (cfg *Config, err error) { cfg = &Config{} f := NewFlagSet(cmdline[0], flag.ExitOnError) // dummy values which were parsed earlier f.String("cfg", "", "Path or URL to config file") f.Bool("v", false, "Show version") f.Bool("version", false, "Show version") // config values var listenerValue string var uiListenerValue string var certSourcesValue string var authSchemesValue string var readTimeout, writeTimeout time.Duration var gzipContentTypesValue string var obsoleteStr string var bgpPeersValue string f.BoolVar(&cfg.Insecure, "insecure", defaultConfig.Insecure, "allow fabio to run as root when set to true") f.IntVar(&cfg.Proxy.MaxConn, "proxy.maxconn", defaultConfig.Proxy.MaxConn, "maximum number of cached connections") f.StringVar(&cfg.Proxy.Strategy, "proxy.strategy", defaultConfig.Proxy.Strategy, "load balancing strategy") f.StringVar(&cfg.Proxy.Matcher, "proxy.matcher", defaultConfig.Proxy.Matcher, "path matching algorithm") f.IntVar(&cfg.Proxy.NoRouteStatus, "proxy.noroutestatus", defaultConfig.Proxy.NoRouteStatus, "status code for invalid route. Must be three digits") f.DurationVar(&cfg.Proxy.ShutdownWait, "proxy.shutdownwait", defaultConfig.Proxy.ShutdownWait, "time for graceful shutdown") f.DurationVar(&cfg.Proxy.DeregisterGracePeriod, "proxy.deregistergraceperiod", defaultConfig.Proxy.DeregisterGracePeriod, "time to wait after deregistering from a registry") f.DurationVar(&cfg.Proxy.DialTimeout, "proxy.dialtimeout", defaultConfig.Proxy.DialTimeout, "connection timeout for backend connections") f.DurationVar(&cfg.Proxy.ResponseHeaderTimeout, "proxy.responseheadertimeout", defaultConfig.Proxy.ResponseHeaderTimeout, "response header timeout") f.DurationVar(&cfg.Proxy.KeepAliveTimeout, "proxy.keepalivetimeout", defaultConfig.Proxy.KeepAliveTimeout, "keep-alive timeout") f.DurationVar(&cfg.Proxy.IdleConnTimeout, "proxy.idleconntimeout", defaultConfig.Proxy.IdleConnTimeout, "idle timeout, when to close (keep-alive) connections") f.StringVar(&cfg.Proxy.LocalIP, "proxy.localip", defaultConfig.Proxy.LocalIP, "fabio address in Forward headers") f.StringVar(&cfg.Proxy.ClientIPHeader, "proxy.header.clientip", defaultConfig.Proxy.ClientIPHeader, "header for the request ip") f.StringVar(&cfg.Proxy.TLSHeader, "proxy.header.tls", defaultConfig.Proxy.TLSHeader, "header for TLS connections") f.StringVar(&cfg.Proxy.TLSHeaderValue, "proxy.header.tls.value", defaultConfig.Proxy.TLSHeaderValue, "value for TLS connection header") f.StringVar(&cfg.Proxy.RequestID, "proxy.header.requestid", defaultConfig.Proxy.RequestID, "header for reqest id") f.IntVar(&cfg.Proxy.STSHeader.MaxAge, "proxy.header.sts.maxage", defaultConfig.Proxy.STSHeader.MaxAge, "enable and set the max-age value for HSTS") f.BoolVar(&cfg.Proxy.STSHeader.Subdomains, "proxy.header.sts.subdomains", defaultConfig.Proxy.STSHeader.Subdomains, "direct HSTS to include subdomains") f.BoolVar(&cfg.Proxy.STSHeader.Preload, "proxy.header.sts.preload", defaultConfig.Proxy.STSHeader.Preload, "direct HSTS to pass the preload directive") f.IntVar(&cfg.Proxy.GRPCMaxRxMsgSize, "proxy.grpcmaxrxmsgsize", defaultConfig.Proxy.GRPCMaxRxMsgSize, "max grpc receive message size (in bytes)") f.IntVar(&cfg.Proxy.GRPCMaxTxMsgSize, "proxy.grpcmaxtxmsgsize", defaultConfig.Proxy.GRPCMaxTxMsgSize, "max grpc transmit message size (in bytes)") f.DurationVar(&cfg.Proxy.GRPCGShutdownTimeout, "proxy.grpcshutdowntimeout", defaultConfig.Proxy.GRPCGShutdownTimeout, "amount of time to wait for graceful shutdown of grpc backend") f.StringVar(&gzipContentTypesValue, "proxy.gzip.contenttype", defaultValues.GZIPContentTypesValue, "regexp of content types to compress") f.StringVar(&listenerValue, "proxy.addr", defaultValues.ListenerValue, "listener config") f.StringVar(&certSourcesValue, "proxy.cs", defaultValues.CertSourcesValue, "certificate sources") f.DurationVar(&readTimeout, "proxy.readtimeout", defaultValues.ReadTimeout, "read timeout for incoming requests") f.DurationVar(&writeTimeout, "proxy.writetimeout", defaultValues.WriteTimeout, "write timeout for outgoing responses") f.DurationVar(&cfg.Proxy.FlushInterval, "proxy.flushinterval", defaultConfig.Proxy.FlushInterval, "flush interval for streaming responses") f.DurationVar(&cfg.Proxy.GlobalFlushInterval, "proxy.globalflushinterval", defaultConfig.Proxy.GlobalFlushInterval, "flush interval for non-streaming responses") f.StringVar(&authSchemesValue, "proxy.auth", defaultValues.AuthSchemesValue, "auth schemes") f.StringVar(&cfg.Log.AccessFormat, "log.access.format", defaultConfig.Log.AccessFormat, "access log format") f.StringVar(&cfg.Log.AccessTarget, "log.access.target", defaultConfig.Log.AccessTarget, "access log target") f.StringVar(&cfg.Log.RoutesFormat, "log.routes.format", defaultConfig.Log.RoutesFormat, "log format of routing table updates") f.StringVar(&cfg.Log.Level, "log.level", defaultConfig.Log.Level, "log level: TRACE, DEBUG, INFO, WARN, ERROR, FATAL") f.StringVar(&cfg.Metrics.Target, "metrics.target", defaultConfig.Metrics.Target, "metrics backend") f.StringVar(&cfg.Metrics.Prefix, "metrics.prefix", defaultConfig.Metrics.Prefix, "prefix for reported metrics") f.StringVar(&cfg.Metrics.Names, "metrics.names", defaultConfig.Metrics.Names, "route metric name template") f.DurationVar(&cfg.Metrics.Interval, "metrics.interval", defaultConfig.Metrics.Interval, "metrics reporting interval") f.DurationVar(&cfg.Metrics.Timeout, "metrics.timeout", defaultConfig.Metrics.Timeout, "timeout for metrics to become available") f.DurationVar(&cfg.Metrics.Retry, "metrics.retry", defaultConfig.Metrics.Retry, "retry interval during startup") f.StringVar(&cfg.Metrics.GraphiteAddr, "metrics.graphite.addr", defaultConfig.Metrics.GraphiteAddr, "graphite server address") f.StringVar(&cfg.Metrics.StatsDAddr, "metrics.statsd.addr", defaultConfig.Metrics.StatsDAddr, "statsd server address") f.StringVar(&cfg.Metrics.DogstatsdAddr, "metrics.dogstatsd.addr", defaultConfig.Metrics.DogstatsdAddr, "dogstatsd server address") f.StringVar(&cfg.Metrics.Circonus.APIKey, "metrics.circonus.apikey", defaultConfig.Metrics.Circonus.APIKey, "Circonus API token key") f.StringVar(&cfg.Metrics.Circonus.APIApp, "metrics.circonus.apiapp", defaultConfig.Metrics.Circonus.APIApp, "Circonus API token app") f.StringVar(&cfg.Metrics.Circonus.APIURL, "metrics.circonus.apiurl", defaultConfig.Metrics.Circonus.APIURL, "Circonus API URL") f.StringVar(&cfg.Metrics.Circonus.BrokerID, "metrics.circonus.brokerid", defaultConfig.Metrics.Circonus.BrokerID, "Circonus Broker ID") f.StringVar(&cfg.Metrics.Circonus.CheckID, "metrics.circonus.checkid", defaultConfig.Metrics.Circonus.CheckID, "Circonus Check ID") f.StringVar(&cfg.Metrics.Circonus.SubmissionURL, "metrics.circonus.submissionurl", defaultConfig.Metrics.Circonus.SubmissionURL, "Circonus Check SubmissionURL") f.StringVar(&cfg.Metrics.Prometheus.Subsystem, "metrics.prometheus.subsystem", defaultConfig.Metrics.Prometheus.Subsystem, "Prometheus system") f.StringVar(&cfg.Metrics.Prometheus.Path, "metrics.prometheus.path", defaultConfig.Metrics.Prometheus.Path, "Prometheus http handler path") f.FloatSliceVar(&cfg.Metrics.Prometheus.Buckets, "metrics.prometheus.buckets", defaultConfig.Metrics.Prometheus.Buckets, "Prometheus histogram buckets") f.StringVar(&cfg.Registry.Backend, "registry.backend", defaultConfig.Registry.Backend, "registry backend") f.DurationVar(&cfg.Registry.Timeout, "registry.timeout", defaultConfig.Registry.Timeout, "timeout for registry to become available") f.DurationVar(&cfg.Registry.Retry, "registry.retry", defaultConfig.Registry.Retry, "retry interval during startup") f.StringVar(&cfg.Registry.File.RoutesPath, "registry.file.path", defaultConfig.Registry.File.RoutesPath, "path to file based routing table") f.StringVar(&cfg.Registry.File.NoRouteHTMLPath, "registry.file.noroutehtmlpath", defaultConfig.Registry.File.NoRouteHTMLPath, "path to file for HTML returned when no route is found") f.StringVar(&cfg.Registry.Static.Routes, "registry.static.routes", defaultConfig.Registry.Static.Routes, "static routes") f.StringVar(&cfg.Registry.Static.NoRouteHTML, "registry.static.noroutehtml", defaultConfig.Registry.Static.NoRouteHTML, "HTML which is returned when no route is found") f.StringVar(&cfg.Registry.Consul.Addr, "registry.consul.addr", defaultConfig.Registry.Consul.Addr, "address of the consul agent") f.StringVar(&cfg.Registry.Consul.Token, "registry.consul.token", defaultConfig.Registry.Consul.Token, "token for consul agent") f.StringVar(&cfg.Registry.Consul.KVPath, "registry.consul.kvpath", defaultConfig.Registry.Consul.KVPath, "consul KV path for manual overrides") f.StringVar(&cfg.Registry.Consul.NoRouteHTMLPath, "registry.consul.noroutehtmlpath", defaultConfig.Registry.Consul.NoRouteHTMLPath, "consul KV path for HTML returned when no route is found") f.StringVar(&cfg.Registry.Consul.TagPrefix, "registry.consul.tagprefix", defaultConfig.Registry.Consul.TagPrefix, "prefix for consul tags") f.StringVar(&cfg.Registry.Consul.TLS.KeyFile, "registry.consul.tls.keyfile", defaultConfig.Registry.Consul.TLS.KeyFile, "path to consul key file") f.StringVar(&cfg.Registry.Consul.TLS.CertFile, "registry.consul.tls.certfile", defaultConfig.Registry.Consul.TLS.CertFile, "path to consul cert file") f.StringVar(&cfg.Registry.Consul.TLS.CAFile, "registry.consul.tls.cafile", defaultConfig.Registry.Consul.TLS.CAFile, "path to consul CA file") f.StringVar(&cfg.Registry.Consul.TLS.CAPath, "registry.consul.tls.capath", defaultConfig.Registry.Consul.TLS.CAPath, "path to consul CA directory") f.BoolVar(&cfg.Registry.Consul.TLS.InsecureSkipVerify, "registry.consul.tls.insecureskipverify", defaultConfig.Registry.Consul.TLS.InsecureSkipVerify, "is tls check enabled") f.BoolVar(&cfg.Registry.Consul.Register, "registry.consul.register.enabled", defaultConfig.Registry.Consul.Register, "register fabio in consul") f.StringVar(&cfg.Registry.Consul.Namespace, "registry.consul.namespace", defaultConfig.Registry.Consul.Namespace, "consul namespace in which fabio is active") f.StringVar(&cfg.Registry.Consul.ServiceAddr, "registry.consul.register.addr", "", "service registration address") f.StringVar(&cfg.Registry.Consul.ServiceName, "registry.consul.register.name", defaultConfig.Registry.Consul.ServiceName, "service registration name") f.StringSliceVar(&cfg.Registry.Consul.ServiceTags, "registry.consul.register.tags", defaultConfig.Registry.Consul.ServiceTags, "service registration tags") f.StringSliceVar(&cfg.Registry.Consul.ServiceStatus, "registry.consul.service.status", defaultConfig.Registry.Consul.ServiceStatus, "valid service status values") f.DurationVar(&cfg.Registry.Consul.CheckInterval, "registry.consul.register.checkInterval", defaultConfig.Registry.Consul.CheckInterval, "service check interval") f.DurationVar(&cfg.Registry.Consul.CheckTimeout, "registry.consul.register.checkTimeout", defaultConfig.Registry.Consul.CheckTimeout, "service check timeout") f.BoolVar(&cfg.Registry.Consul.CheckTLSSkipVerify, "registry.consul.register.checkTLSSkipVerify", defaultConfig.Registry.Consul.CheckTLSSkipVerify, "service check TLS verification") f.StringVar(&obsoleteStr, "registry.consul.register.checkDeregisterCriticalServiceAfter", "", "This option is deprecated and has no effect.") f.StringVar(&cfg.Registry.Consul.ChecksRequired, "registry.consul.checksRequired", defaultConfig.Registry.Consul.ChecksRequired, "number of checks which must pass: one or all") f.IntVar(&cfg.Registry.Consul.ServiceMonitors, "registry.consul.serviceMonitors", defaultConfig.Registry.Consul.ServiceMonitors, "concurrency for route updates") f.DurationVar(&cfg.Registry.Consul.PollInterval, "registry.consul.pollinterval", defaultConfig.Registry.Consul.PollInterval, "poll interval for route updates") f.BoolVar(&cfg.Registry.Consul.RequireConsistent, "registry.consul.requireConsistent", defaultConfig.Registry.Consul.RequireConsistent, "is consistent read mode on consul queries required") f.BoolVar(&cfg.Registry.Consul.AllowStale, "registry.consul.allowStale", defaultConfig.Registry.Consul.AllowStale, "is stale read mode on consul queries allowed") f.IntVar(&cfg.Runtime.GOGC, "runtime.gogc", defaultConfig.Runtime.GOGC, "sets runtime.GOGC") f.IntVar(&cfg.Runtime.GOMAXPROCS, "runtime.gomaxprocs", defaultConfig.Runtime.GOMAXPROCS, "sets runtime.GOMAXPROCS") f.StringVar(&cfg.UI.Access, "ui.access", defaultConfig.UI.Access, "access mode, one of [ro, rw]") f.StringVar(&uiListenerValue, "ui.addr", defaultValues.UIListenerValue, "Address the UI/API is listening on") f.StringVar(&cfg.UI.Color, "ui.color", defaultConfig.UI.Color, "background color of the UI") f.StringVar(&cfg.UI.Title, "ui.title", defaultConfig.UI.Title, "optional title for the UI") f.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") f.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") f.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") f.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") f.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") f.StringVar(&cfg.ProfileMode, "profile.mode", defaultConfig.ProfileMode, "enable profiling mode, one of [cpu, mem, mutex, block, trace]") f.StringVar(&cfg.ProfilePath, "profile.path", defaultConfig.ProfilePath, "path to profile dump file") f.BoolVar(&cfg.GlobMatchingDisabled, "glob.matching.disabled", defaultConfig.GlobMatchingDisabled, "Disable Glob Matching on routes, one of [true, false]") f.IntVar(&cfg.GlobCacheSize, "glob.cache.size", defaultConfig.GlobCacheSize, "sets the size of the glob cache") f.StringVar(&cfg.Registry.Custom.Host, "registry.custom.host", defaultConfig.Registry.Custom.Host, "custom back end hostname/port") f.StringVar(&cfg.Registry.Custom.Scheme, "registry.custom.scheme", defaultConfig.Registry.Custom.Scheme, "custom back end scheme - http/https") f.StringVar(&cfg.Registry.Custom.NoRouteHTML, "registry.custom.noroutehtml", defaultConfig.Registry.Custom.NoRouteHTML, "path to file for HTML returned when no route is found") f.BoolVar(&cfg.Registry.Custom.CheckTLSSkipVerify, "registry.custom.checkTLSSkipVerify", defaultConfig.Registry.Custom.CheckTLSSkipVerify, "custom back end check TLS verification") f.DurationVar(&cfg.Registry.Custom.Timeout, "registry.custom.timeout", defaultConfig.Registry.Custom.Timeout, "timeout for API request to custom back end") f.DurationVar(&cfg.Registry.Custom.PollInterval, "registry.custom.pollinterval", defaultConfig.Registry.Custom.PollInterval, "poll interval for API request to custom back end") f.StringVar(&cfg.Registry.Custom.Path, "registry.custom.path", defaultConfig.Registry.Custom.Path, "custom back end path in the URL") f.StringVar(&cfg.Registry.Custom.QueryParams, "registry.custom.queryparams", defaultConfig.Registry.Custom.QueryParams, "custom back end query parameters in the URL") f.BoolVar(&cfg.BGP.BGPEnabled, "bgp.enabled", defaultConfig.BGP.BGPEnabled, "enabled bgp announcements") f.UintVar(&cfg.BGP.Asn, "bgp.asn", defaultConfig.BGP.Asn, "our BGP asn") f.StringSliceVar(&cfg.BGP.AnycastAddresses, "bgp.anycastaddresses", defaultConfig.BGP.AnycastAddresses, "comma separated list of CIDRs to broadcast - required if bgp is enabled") f.StringVar(&cfg.BGP.RouterID, "bgp.routerid", defaultConfig.BGP.RouterID, "our router ID - required if bgp is enabled") f.IntVar(&cfg.BGP.ListenPort, "bgp.listenport", defaultConfig.BGP.ListenPort, "bgp listen port. -1 means disabled") f.StringSliceVar(&cfg.BGP.ListenAddresses, "bgp.listenaddresses", defaultConfig.BGP.ListenAddresses, "bgp listen address") f.StringVar(&bgpPeersValue, "bgp.peers", defaultValues.BGPPeersValue, "bgp peers. comma separated list of neighboraddress=1.2.3.4;asn=65001") f.BoolVar(&cfg.BGP.EnableGRPC, "bgp.enablegrpc", defaultConfig.BGP.EnableGRPC, "enable bgp grpc listener for use with gobgp cli") f.StringVar(&cfg.BGP.GRPCListenAddress, "bgp.grpclistenaddress", defaultConfig.BGP.GRPCListenAddress, "bgp grpc cli listen address") f.StringVar(&cfg.BGP.NextHop, "bgp.nexthop", defaultConfig.BGP.NextHop, "specify the next-hop. defaults to bgp.routerid") f.StringVar(&cfg.BGP.GOBGPDCfgFile, "bgp.gobgpdcfgfile", defaultConfig.BGP.GOBGPDCfgFile, "specify path to gobgpd config file. overrides settings") // deprecated flags var proxyLogRoutes string f.StringVar(&proxyLogRoutes, "proxy.log.routes", "", "deprecated. use log.routes.format instead") var awsApiGWCertCN string f.StringVar(&awsApiGWCertCN, "aws.apigw.cert.cn", "", "deprecated. use caupgcn= for cert source") // parse configuration if err := f.ParseFlags(cmdline[1:], environ, envprefix, props); err != nil { return nil, err } // post configuration if cfg.Runtime.GOMAXPROCS == -1 { cfg.Runtime.GOMAXPROCS = runtime.NumCPU() } cfg.Registry.Consul.Scheme, cfg.Registry.Consul.Addr = parseScheme(cfg.Registry.Consul.Addr) certSources, err := parseCertSources(certSourcesValue) if err != nil { return nil, err } authSchemes, err := parseAuthSchemes(authSchemesValue) if err != nil { return nil, err } cfg.Proxy.AuthSchemes = authSchemes if uiListenerValue != "" { kvs, err := parseKVSlice(uiListenerValue) if err != nil { return nil, err } if len(kvs) != 1 { return nil, fmt.Errorf("ui.addr must contain only one listener") } cfg.UI.Listen, err = parseListen(kvs[0], certSources, 0, 0) if err != nil { return nil, err } } cfg.Listen, err = parseListeners(listenerValue, certSources, readTimeout, writeTimeout) if err != nil { return nil, err } if cfg.Proxy.LocalIP != "" { if cfg.Proxy.LocalIP, err = gs.Parse(cfg.Proxy.LocalIP); err != nil { return nil, fmt.Errorf("failed to parse local ip: %s", err) } } // Unless registry.consul.register.addr has been set explicitly it should // be the same as ui.addr. See issue 657. if !f.IsSet("registry.consul.register.addr") { cfg.Registry.Consul.ServiceAddr = cfg.UI.Listen.Addr } if cfg.Registry.Consul.ServiceAddr != "" { if cfg.Registry.Consul.ServiceAddr, err = gs.Parse(cfg.Registry.Consul.ServiceAddr); err != nil { return nil, fmt.Errorf("failed to consul service address: %s", err) } } cfg.Registry.Consul.CheckScheme = defaultConfig.Registry.Consul.CheckScheme if cfg.UI.Listen.CertSource.Name != "" { cfg.Registry.Consul.CheckScheme = "https" } if cfg.Registry.Consul.ServiceMonitors <= 0 { cfg.Registry.Consul.ServiceMonitors = 1 } if gzipContentTypesValue != "" { cfg.Proxy.GZIPContentTypes, err = regexp.Compile(gzipContentTypesValue) if err != nil { return nil, fmt.Errorf("invalid expression for content types: %s", err) } } if cfg.Proxy.Strategy != "rr" && cfg.Proxy.Strategy != "rnd" { return nil, fmt.Errorf("invalid proxy.strategy: %s", cfg.Proxy.Strategy) } if cfg.Proxy.Matcher != "prefix" && cfg.Proxy.Matcher != "glob" && cfg.Proxy.Matcher != "iprefix" { return nil, fmt.Errorf("invalid proxy.matcher: %s", cfg.Proxy.Matcher) } if cfg.UI.Access != "ro" && cfg.UI.Access != "rw" { return nil, fmt.Errorf("invalid ui.access: %s", cfg.UI.Access) } // go1.10 will not accept a non-three digit status code if cfg.Proxy.NoRouteStatus < 100 || cfg.Proxy.NoRouteStatus > 999 { return nil, fmt.Errorf("proxy.noroutestatus must be between 100 and 999") } if cfg.Registry.Consul.AllowStale && cfg.Registry.Consul.RequireConsistent { return nil, fmt.Errorf("registry.consul.allowStale and registry.consul.requireConsistent cannot both be true") } // handle deprecations deprecate := func(name, msg string) { if f.IsSet(name) { log.Print("[WARN] ", msg) } } deprecate("proxy.log.routes", "proxy.log.routes has been deprecated. Please use 'log.routes.format' instead") if proxyLogRoutes != "" { cfg.Log.RoutesFormat = proxyLogRoutes } cfg.BGP.Peers, err = parseBGPPeers(bgpPeersValue) if err != nil { return nil, err } return cfg, nil } // parseScheme splits a url into scheme and address and defaults // to "http" if no scheme was given. func parseScheme(s string) (scheme, addr string) { s = strings.ToLower(s) switch { case strings.HasPrefix(s, "https://"): scheme, addr = "https", s[len("https://"):] case strings.HasPrefix(s, "http://"): scheme, addr = "http", s[len("http://"):] default: scheme, addr = "http", s } // strip off anything after a final slash if n := strings.Index(addr, "/"); n >= 0 { addr = addr[:n] } return } func parseListeners(cfgs string, cs map[string]CertSource, readTimeout, writeTimeout time.Duration) (listen []Listen, err error) { kvs, err := parseKVSlice(cfgs) for _, cfg := range kvs { l, err := parseListen(cfg, cs, readTimeout, writeTimeout) if err != nil { return nil, err } listen = append(listen, l) } return } func parseListen(cfg map[string]string, cs map[string]CertSource, readTimeout, writeTimeout time.Duration) (l Listen, err error) { l = Listen{ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, } var csName string for k, v := range cfg { switch k { case "", "addr": if l.Addr, err = gs.Parse(v); err != nil { return Listen{}, err } case "proto": l.Proto = v switch l.Proto { case "tcp", "tcp+sni", "tcp-dynamic", "http", "https", "grpc", "grpcs", "https+tcp+sni", "prometheus": // ok default: return Listen{}, fmt.Errorf("unknown protocol %q", v) } case "rt": // read timeout d, err := time.ParseDuration(v) if err != nil { return Listen{}, err } l.ReadTimeout = d case "wt": // write timeout d, err := time.ParseDuration(v) if err != nil { return Listen{}, err } l.WriteTimeout = d case "it": // idle timeout d, err := time.ParseDuration(v) if err != nil { return Listen{}, err } l.IdleTimeout = d case "cs": // cert source csName = v c, ok := cs[v] if !ok { return Listen{}, fmt.Errorf("unknown certificate source %q", v) } l.CertSource = c if l.Proto == "" { l.Proto = "https" } case "strictmatch": l.StrictMatch = (v == "true") case "tlsmin": n, err := parseTLSVersion(v) if err != nil { return Listen{}, err } l.TLSMinVersion = n case "tlsmax": n, err := parseTLSVersion(v) if err != nil { return Listen{}, err } l.TLSMaxVersion = n case "tlsciphers": c, err := parseTLSCiphers(v) if err != nil { return Listen{}, err } l.TLSCiphers = c case "pxyproto": l.ProxyProto = (v == "true") case "pxytimeout": d, err := time.ParseDuration(v) if err != nil { return Listen{}, err } l.ProxyHeaderTimeout = d case "refresh": d, err := time.ParseDuration(v) if err != nil { return Listen{}, err } l.Refresh = d } } if l.Proto == "" { l.Proto = "http" } if l.Addr == "" { return Listen{}, fmt.Errorf("need listening host:port") } if csName != "" && l.Proto != "https" && l.Proto != "tcp" && l.Proto != "tcp-dynamic" && l.Proto != "grpcs" && l.Proto != "prometheus" && l.Proto != "https+tcp+sni" { return Listen{}, fmt.Errorf("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'") } if csName == "" && l.Proto == "https" { return Listen{}, fmt.Errorf("proto 'https' requires cert source") } if csName == "" && l.Proto == "grpcs" { return Listen{}, fmt.Errorf("proto 'grpcs' requires cert source") } if cs[csName].Type == "vault-pki" && !l.StrictMatch { // Without StrictMatch the first issued certificate is used for all // subsequent requests, even if the common name doesn't match. log.Printf("[INFO] vault-pki requires strictmatch; enabling strictmatch for listener %s", l.Addr) l.StrictMatch = true } if l.ProxyProto && l.ProxyHeaderTimeout == 0 { // We should define a safe default if proxy-protocol was enabled but no header timeout was set. // See https://github.com/fabiolb/fabio/issues/524 for more information. l.ProxyHeaderTimeout = 250 * time.Millisecond } return } var tlsver = map[string]uint16{ "tls10": tls.VersionTLS10, "tls11": tls.VersionTLS11, "tls12": tls.VersionTLS12, "tls13": tls.VersionTLS13, } func parseTLSVersion(s string) (uint16, error) { s = strings.ToLower(strings.TrimSpace(s)) if n, ok := tlsver[s]; ok { return n, nil } return parseUint16(s) } func parseTLSCiphers(s string) ([]uint16, error) { var c []uint16 for _, v := range strings.Split(s, ",") { v = strings.ToUpper(strings.TrimSpace(v)) if n, ok := tlsciphers[v]; ok { c = append(c, n) continue } n, err := parseUint16(v) if err != nil { return nil, err } c = append(c, n) } return c, nil } func parseUint16(s string) (uint16, error) { n, err := strconv.ParseUint(s, 0, 16) if err != nil { return 0, err } return uint16(n), nil } func parseCertSources(cfgs string) (cs map[string]CertSource, err error) { kvs, err := parseKVSlice(cfgs) if err != nil { return nil, err } cs = map[string]CertSource{} for _, cfg := range kvs { src, err := parseCertSource(cfg) if err != nil { return nil, err } cs[src.Name] = src } return } func parseCertSource(cfg map[string]string) (c CertSource, err error) { if cfg == nil { return CertSource{}, nil } c.Refresh = 3 * time.Second for k, v := range cfg { switch k { case "cs": c.Name = v case "type": c.Type = v case "cert": c.CertPath = v case "key": c.KeyPath = v case "clientca": c.ClientCAPath = v case "caupgcn": c.CAUpgradeCN = v case "refresh": d, err := time.ParseDuration(v) if err != nil { return CertSource{}, err } c.Refresh = d case "vaultfetchtoken": c.VaultFetchToken = v case "hdr": p := strings.SplitN(v, ": ", 2) if len(p) != 2 { return CertSource{}, fmt.Errorf("invalid header %s", v) } if c.Header == nil { c.Header = http.Header{} } c.Header.Set(p[0], p[1]) } } if c.Name == "" { return CertSource{}, fmt.Errorf("missing 'cs' in %s", cfg) } if c.CertPath == "" { return CertSource{}, fmt.Errorf("missing 'cert' in %s", cfg) } switch c.Type { case "": return CertSource{}, fmt.Errorf("missing 'type' in %s", cfg) case "file", "consul": c.Refresh = 0 case "path", "http", "vault", "vault-pki": // no-op default: return CertSource{}, fmt.Errorf("unknown cert source type %s", c.Type) } return } func parseAuthSchemes(cfgs string) (as map[string]AuthScheme, err error) { kvs, err := parseKVSlice(cfgs) if err != nil { return nil, err } as = map[string]AuthScheme{} for _, cfg := range kvs { src, err := parseAuthScheme(cfg) if err != nil { return nil, err } as[src.Name] = src } return } func parseAuthScheme(cfg map[string]string) (a AuthScheme, err error) { if cfg == nil { return } for k, v := range cfg { switch k { case "name": a.Name = v case "type": a.Type = v } } if a.Name == "" { return AuthScheme{}, errors.New("missing 'name' in auth") } switch a.Type { case "": return AuthScheme{}, fmt.Errorf("missing 'type' in auth '%s'", a.Name) case "basic": a.Basic = BasicAuth{ File: cfg["file"], Realm: cfg["realm"], Refresh: 0, // the htpasswd file refresh is disabled by default } if a.Basic.File == "" { return AuthScheme{}, fmt.Errorf("missing 'file' in auth '%s'", a.Name) } if a.Basic.Realm == "" { a.Basic.Realm = a.Name } if cfg["refresh"] != "" { d, err := time.ParseDuration(cfg["refresh"]) if err != nil { return AuthScheme{}, err } if d < time.Second { d = time.Second } a.Basic.Refresh = d } default: return AuthScheme{}, fmt.Errorf("unknown auth type '%s'", a.Type) } return } func parseBGPPeers(cfgs string) ([]BGPPeer, error) { kvs, err := parseKVSlice(cfgs) if err != nil { return nil, err } var peers []BGPPeer for _, cfg := range kvs { peer, err := parseBGPPeer(cfg) if err != nil { return nil, err } peers = append(peers, peer) } return peers, nil } func parseBGPPeer(cfg map[string]string) (BGPPeer, error) { var peer = *defaultBGPPeer for k, v := range cfg { switch k { case "address": peer.NeighborAddress = v case "port": u, err := strconv.ParseUint(v, 10, 32) if err != nil { return peer, err } peer.NeighborPort = uint(u) case "asn": u, err := strconv.ParseUint(v, 10, 32) if err != nil { return peer, err } peer.Asn = uint(u) case "multihop": b, err := strconv.ParseBool(v) if err != nil { return peer, err } peer.MultiHop = b case "multihoplength": u, err := strconv.ParseUint(v, 10, 32) if err != nil { return peer, err } peer.MultiHopLength = uint(u) case "password": peer.Password = v } } return peer, nil } ================================================ FILE: config/load_test.go ================================================ package config import ( "crypto/tls" "errors" "fmt" "net/http" "net/http/httptest" "os" "reflect" "regexp" "strings" "testing" "time" "github.com/pascaldekloe/goe/verify" ) func TestLoad(t *testing.T) { tests := []struct { desc string args []string environ []string path string data string cfg func(*Config) *Config err error }{ { args: []string{"-v"}, cfg: func(cfg *Config) *Config { return nil }, }, { args: []string{"--version"}, cfg: func(cfg *Config) *Config { return nil }, }, { desc: "-v with other args", args: []string{"-a", "-v", "-b"}, cfg: func(cfg *Config) *Config { return nil }, }, { desc: "--version with other args", args: []string{"-a", "--version", "-b"}, cfg: func(cfg *Config) *Config { return nil }, }, { desc: "default config", cfg: func(cfg *Config) *Config { return cfg }, }, { args: []string{"-insecure=true"}, cfg: func(cfg *Config) *Config { cfg.Insecure = true return cfg }, }, { args: []string{"-profile.mode", "foo"}, cfg: func(cfg *Config) *Config { cfg.ProfileMode = "foo" return cfg }, }, { args: []string{"-profile.path", "foo"}, cfg: func(cfg *Config) *Config { cfg.ProfilePath = "foo" return cfg }, }, { args: []string{"-proxy.addr", ":5555"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { args: []string{"-proxy.addr", ":5555;proto=http"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { args: []string{"-proxy.addr", ":5555;proto=tcp"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "tcp"}} return cfg }, }, { args: []string{"-proxy.addr", ":5555;proto=tcp+sni"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "tcp+sni"}} return cfg }, }, { args: []string{"-proxy.addr", ":5555;proto=grpc"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "grpc"}} return cfg }, }, { args: []string{"-proxy.addr", ":5555;proto=tcp-dynamic"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "tcp-dynamic"}} return cfg }, }, { desc: "-proxy.addr with tls configs", args: []string{"-proxy.addr", `:5555;rt=1s;wt=2s;it=3s;tlsmin=0x0300;tlsmax=0x305;tlsciphers="0x123,0x456"`}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ { Addr: ":5555", Proto: "http", ReadTimeout: 1 * time.Second, WriteTimeout: 2 * time.Second, IdleTimeout: 3 * time.Second, TLSMinVersion: 0x300, TLSMaxVersion: 0x305, TLSCiphers: []uint16{0x123, 0x456}, }, } return cfg }, }, { desc: "-proxy.addr with named tls configs", args: []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"`}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ { Addr: ":5555", Proto: "http", ReadTimeout: 1 * time.Second, WriteTimeout: 2 * time.Second, IdleTimeout: 3 * time.Second, TLSMinVersion: tls.VersionTLS10, TLSMaxVersion: tls.VersionTLS11, TLSCiphers: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384}, }, } return cfg }, }, { desc: "-proxy.addr with file cert source", args: []string{"-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=file;cert=value"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "file", CertPath: "value"} return cfg }, }, { desc: "-proxy.addr with path cert source", args: []string{"-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=path;cert=value"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "path", CertPath: "value", Refresh: 3 * time.Second} return cfg }, }, { desc: "-proxy.addr with http cert source", args: []string{"-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=http;cert=value"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "http", CertPath: "value", Refresh: 3 * time.Second} return cfg }, }, { desc: "-proxy.addr with consul cert source", args: []string{"-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=consul;cert=value"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "consul", CertPath: "value"} return cfg }, }, { desc: "-proxy.addr with vault cert source", args: []string{"-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=vault;cert=value"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "vault", CertPath: "value", Refresh: 3 * time.Second} return cfg }, }, { desc: "-proxy.addr with vault-pki cert source", args: []string{ "-proxy.addr", ":5555;cs=name", "-proxy.cs", "cs=name;type=vault-pki;cert=pki/issue/value", }, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "vault-pki", CertPath: "pki/issue/value", Refresh: 3 * time.Second} cfg.Listen[0].StrictMatch = true // implicit return cfg }, }, { desc: "-proxy.addr with vault-pki cert source, -proxy.cs first", args: []string{ "-proxy.cs", "cs=name;type=vault-pki;cert=pki/issue/value", "-proxy.addr", ":5555;cs=name", }, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "https"}} cfg.Listen[0].CertSource = CertSource{Name: "name", Type: "vault-pki", CertPath: "pki/issue/value", Refresh: 3 * time.Second} cfg.Listen[0].StrictMatch = true // implicit return cfg }, }, { desc: "-proxy.addr with cert source", args: []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"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ { Addr: ":5555", Proto: "https", StrictMatch: true, CertSource: CertSource{ Name: "name", Type: "path", CertPath: "foo", ClientCAPath: "bar", Refresh: 2 * time.Second, Header: http.Header{"A": []string{"b"}}, CAUpgradeCN: "furb", }, }, } return cfg }, }, { desc: "-proxy.addr with cert source with full options", args: []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"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ { Addr: ":5555", Proto: "https", StrictMatch: true, CertSource: CertSource{ Name: "name", Type: "path", CertPath: "foo", ClientCAPath: "bar", Refresh: 2 * time.Second, Header: http.Header{"A": []string{"b"}}, CAUpgradeCN: "furb", }, }, } return cfg }, }, { desc: "-proxy.auth with source basic", args: []string{"-proxy.auth", "name=foo;type=basic;file=/some/file/on/disk;realm=realm"}, cfg: func(cfg *Config) *Config { cfg.Proxy.AuthSchemes = map[string]AuthScheme{ "foo": { Name: "foo", Type: "basic", Basic: BasicAuth{ File: "/some/file/on/disk", Realm: "realm", }, }, } return cfg }, }, { desc: "-proxy.addr with prometheus and https", args: []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"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ { Addr: ":5555", Proto: "prometheus", StrictMatch: true, CertSource: CertSource{ Name: "name", Type: "path", CertPath: "foo", ClientCAPath: "bar", Refresh: 2 * time.Second, Header: http.Header{"A": []string{"b"}}, CAUpgradeCN: "furb", }, }, } return cfg }, }, { desc: "-proxy.auth with source basic and no realm specified", args: []string{"-proxy.auth", "name=foo;type=basic;file=/some/file/on/disk"}, cfg: func(cfg *Config) *Config { cfg.Proxy.AuthSchemes = map[string]AuthScheme{ "foo": { Name: "foo", Type: "basic", Basic: BasicAuth{ File: "/some/file/on/disk", Realm: "foo", }, }, } return cfg }, }, { desc: "issue 305", args: []string{ "-proxy.addr", ":443;cs=consul-cs,:80,:2375;proto=tcp+sni", "-proxy.cs", "cs=consul-cs;type=consul;cert=http://localhost:8500/v1/kv/ssl?token=token", }, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{ {Addr: ":443", Proto: "https"}, {Addr: ":80", Proto: "http"}, {Addr: ":2375", Proto: "tcp+sni"}, } cfg.Listen[0].CertSource = CertSource{ Name: "consul-cs", Type: "consul", CertPath: "http://localhost:8500/v1/kv/ssl?token=token", } return cfg }, }, { args: []string{"-proxy.localip", "1.2.3.4"}, cfg: func(cfg *Config) *Config { cfg.Proxy.LocalIP = "1.2.3.4" return cfg }, }, { args: []string{"-proxy.strategy", "rnd"}, cfg: func(cfg *Config) *Config { cfg.Proxy.Strategy = "rnd" return cfg }, }, { args: []string{"-proxy.strategy", "rr"}, cfg: func(cfg *Config) *Config { cfg.Proxy.Strategy = "rr" return cfg }, }, { args: []string{"-proxy.matcher", "prefix"}, cfg: func(cfg *Config) *Config { cfg.Proxy.Matcher = "prefix" return cfg }, }, { args: []string{"-proxy.matcher", "glob"}, cfg: func(cfg *Config) *Config { cfg.Proxy.Matcher = "glob" return cfg }, }, { args: []string{"-proxy.matcher", "iprefix"}, cfg: func(cfg *Config) *Config { cfg.Proxy.Matcher = "iprefix" return cfg }, }, { args: []string{"-proxy.noroutestatus", "555"}, cfg: func(cfg *Config) *Config { cfg.Proxy.NoRouteStatus = 555 return cfg }, }, { args: []string{"-proxy.shutdownwait", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.ShutdownWait = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.responseheadertimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.ResponseHeaderTimeout = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.keepalivetimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.KeepAliveTimeout = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.dialtimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.DialTimeout = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.readtimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":9999", Proto: "http", ReadTimeout: 5 * time.Millisecond}} return cfg }, }, { args: []string{"-proxy.writetimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":9999", Proto: "http", WriteTimeout: 5 * time.Millisecond}} return cfg }, }, { args: []string{"-proxy.flushinterval", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.FlushInterval = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.globalflushinterval", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Proxy.GlobalFlushInterval = 5 * time.Millisecond return cfg }, }, { args: []string{"-proxy.maxconn", "555"}, cfg: func(cfg *Config) *Config { cfg.Proxy.MaxConn = 555 return cfg }, }, { args: []string{"-proxy.header.clientip", "value"}, cfg: func(cfg *Config) *Config { cfg.Proxy.ClientIPHeader = "value" return cfg }, }, { args: []string{"-proxy.header.tls", "value"}, cfg: func(cfg *Config) *Config { cfg.Proxy.TLSHeader = "value" return cfg }, }, { args: []string{"-proxy.header.tls.value", "value"}, cfg: func(cfg *Config) *Config { cfg.Proxy.TLSHeaderValue = "value" return cfg }, }, { args: []string{"-proxy.header.requestid", "value"}, cfg: func(cfg *Config) *Config { cfg.Proxy.RequestID = "value" return cfg }, }, { args: []string{"-proxy.header.sts.maxage", "31536000"}, cfg: func(cfg *Config) *Config { cfg.Proxy.STSHeader.MaxAge = 31536000 return cfg }, }, { args: []string{"-proxy.header.sts.subdomains", "true"}, cfg: func(cfg *Config) *Config { cfg.Proxy.STSHeader.Subdomains = true return cfg }, }, { args: []string{"-proxy.header.sts.preload", "true"}, cfg: func(cfg *Config) *Config { cfg.Proxy.STSHeader.Preload = true return cfg }, }, { args: []string{"-proxy.gzip.contenttype", `^text/.*$`}, cfg: func(cfg *Config) *Config { cfg.Proxy.GZIPContentTypes = regexp.MustCompile(`^text/.*$`) return cfg }, }, { args: []string{"-proxy.gzip.contenttype", "^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$"}, cfg: func(cfg *Config) *Config { cfg.Proxy.GZIPContentTypes = regexp.MustCompile(`^(text/.*|application/(javascript|json|font-woff|xml)|.*\+(json|xml))(;.*)?$`) return cfg }, }, { args: []string{"-proxy.log.routes", "foobar"}, cfg: func(cfg *Config) *Config { cfg.Log.RoutesFormat = "foobar" return cfg }, }, { args: []string{"-registry.backend", "value"}, cfg: func(cfg *Config) *Config { cfg.Registry.Backend = "value" return cfg }, }, { args: []string{"-registry.timeout", "5s"}, cfg: func(cfg *Config) *Config { cfg.Registry.Timeout = 5 * time.Second return cfg }, }, { args: []string{"-registry.retry", "500ms"}, cfg: func(cfg *Config) *Config { cfg.Registry.Retry = 500 * time.Millisecond return cfg }, }, { args: []string{"-registry.file.path", "value"}, cfg: func(cfg *Config) *Config { cfg.Registry.File.RoutesPath = "value" return cfg }, }, { args: []string{"-registry.file.noroutehtmlpath", "value"}, cfg: func(cfg *Config) *Config { cfg.Registry.File.NoRouteHTMLPath = "value" return cfg }, }, { args: []string{"-registry.static.routes", "value"}, cfg: func(cfg *Config) *Config { cfg.Registry.Static.Routes = "value" return cfg }, }, { args: []string{"-registry.static.noroutehtml", "value"}, cfg: func(cfg *Config) *Config { cfg.Registry.Static.NoRouteHTML = "value" return cfg }, }, { args: []string{"-registry.consul.addr", "1.2.3.4:5555"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Addr = "1.2.3.4:5555" cfg.Registry.Consul.Scheme = "http" return cfg }, }, { args: []string{"-registry.consul.addr", "http://1.2.3.4:5555/"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Addr = "1.2.3.4:5555" cfg.Registry.Consul.Scheme = "http" return cfg }, }, { args: []string{"-registry.consul.addr", "https://1.2.3.4:5555/"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Addr = "1.2.3.4:5555" cfg.Registry.Consul.Scheme = "https" return cfg }, }, { args: []string{"-registry.consul.addr", "HTTPS://1.2.3.4:5555/"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Addr = "1.2.3.4:5555" cfg.Registry.Consul.Scheme = "https" return cfg }, }, { args: []string{"-registry.consul.token", "some-token"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Token = "some-token" return cfg }, }, { args: []string{"-registry.consul.kvpath", "/some/path"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.KVPath = "/some/path" return cfg }, }, { args: []string{"-registry.consul.noroutehtmlpath", "/some/path"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.NoRouteHTMLPath = "/some/path" return cfg }, }, { args: []string{"-registry.consul.tagprefix", "p-"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.TagPrefix = "p-" return cfg }, }, { args: []string{"-registry.consul.register.enabled=false"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Register = false return cfg }, }, { args: []string{"-registry.consul.namespace", "ns1"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.Namespace = "ns1" return cfg }, }, { args: []string{"-registry.consul.register.addr", "1.2.3.4:5555"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.ServiceAddr = "1.2.3.4:5555" return cfg }, }, { args: []string{"-registry.consul.register.name", "fab"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.ServiceName = "fab" return cfg }, }, { args: []string{"-registry.consul.register.checkTLSSkipVerify=true"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.CheckTLSSkipVerify = true return cfg }, }, { args: []string{"-registry.consul.register.tags", "a, b, c, "}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.ServiceTags = []string{"a", "b", "c"} return cfg }, }, { args: []string{"-registry.consul.register.checkInterval", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.CheckInterval = 5 * time.Millisecond return cfg }, }, { args: []string{"-registry.consul.register.checkTimeout", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.CheckTimeout = 5 * time.Millisecond return cfg }, }, { args: []string{"-registry.consul.service.status", "a, b, "}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.ServiceStatus = []string{"a", "b"} return cfg }, }, { args: []string{"-registry.consul.serviceMonitors", "5"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.ServiceMonitors = 5 return cfg }, }, { args: []string{"-registry.custom.host", "localhost:8080"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.Host = "localhost:8080" return cfg }, }, { args: []string{"-registry.custom.scheme", "https"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.Scheme = "https" return cfg }, }, { args: []string{"-registry.custom.checkTLSSkipVerify", "true"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.CheckTLSSkipVerify = true return cfg }, }, { args: []string{"-registry.custom.timeout", "5s"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.Timeout = 5 * time.Second return cfg }, }, { args: []string{"-registry.custom.pollinterval", "5s"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.PollInterval = 5 * time.Second return cfg }, }, { args: []string{"-registry.custom.path", "test"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.Path = "test" return cfg }, }, { args: []string{"-registry.custom.queryparams", "test=1"}, cfg: func(cfg *Config) *Config { cfg.Registry.Custom.QueryParams = "test=1" return cfg }, }, { args: []string{"-registry.consul.pollinterval", "5s"}, cfg: func(cfg *Config) *Config { cfg.Registry.Consul.PollInterval = 5 * time.Second return cfg }, }, { args: []string{"-log.access.format", "foobar"}, cfg: func(cfg *Config) *Config { cfg.Log.AccessFormat = "foobar" return cfg }, }, { args: []string{"-log.access.target", "foobar"}, cfg: func(cfg *Config) *Config { cfg.Log.AccessTarget = "foobar" return cfg }, }, { args: []string{"-log.routes.format", "foobar"}, cfg: func(cfg *Config) *Config { cfg.Log.RoutesFormat = "foobar" return cfg }, }, { args: []string{"-log.level", "foobar"}, cfg: func(cfg *Config) *Config { cfg.Log.Level = "foobar" return cfg }, }, { args: []string{"-metrics.target", "some-target"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Target = "some-target" return cfg }, }, { args: []string{"-metrics.prefix", "some-prefix"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Prefix = "some-prefix" return cfg }, }, { args: []string{"-metrics.names", "some names"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Names = "some names" return cfg }, }, { args: []string{"-metrics.interval", "5ms"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Interval = 5 * time.Millisecond return cfg }, }, { args: []string{"-metrics.timeout", "5s"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Timeout = 5 * time.Second return cfg }, }, { args: []string{"-metrics.retry", "500ms"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Retry = 500 * time.Millisecond return cfg }, }, { args: []string{"-metrics.graphite.addr", "1.2.3.4:5555"}, cfg: func(cfg *Config) *Config { cfg.Metrics.GraphiteAddr = "1.2.3.4:5555" return cfg }, }, { args: []string{"-metrics.statsd.addr", "1.2.3.4:5555"}, cfg: func(cfg *Config) *Config { cfg.Metrics.StatsDAddr = "1.2.3.4:5555" return cfg }, }, { args: []string{"-metrics.circonus.apiapp", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.APIApp = "value" return cfg }, }, { args: []string{"-metrics.circonus.apikey", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.APIKey = "value" return cfg }, }, { args: []string{"-metrics.circonus.apiurl", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.APIURL = "value" return cfg }, }, { args: []string{"-metrics.circonus.brokerid", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.BrokerID = "value" return cfg }, }, { args: []string{"-metrics.circonus.checkid", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.CheckID = "value" return cfg }, }, { args: []string{"-metrics.circonus.submissionurl", "value"}, cfg: func(cfg *Config) *Config { cfg.Metrics.Circonus.SubmissionURL = "value" return cfg }, }, { args: []string{"-runtime.gogc", "555"}, cfg: func(cfg *Config) *Config { cfg.Runtime.GOGC = 555 return cfg }, }, { args: []string{"-runtime.gomaxprocs", "555"}, cfg: func(cfg *Config) *Config { cfg.Runtime.GOMAXPROCS = 555 return cfg }, }, { args: []string{"-ui.access", "ro"}, cfg: func(cfg *Config) *Config { cfg.UI.Access = "ro" return cfg }, }, { args: []string{"-ui.access", "rw"}, cfg: func(cfg *Config) *Config { cfg.UI.Access = "rw" return cfg }, }, { args: []string{"-ui.addr", "1.2.3.4:5555"}, cfg: func(cfg *Config) *Config { cfg.UI.Listen.Addr = "1.2.3.4:5555" cfg.UI.Listen.Proto = "http" cfg.Registry.Consul.ServiceAddr = "1.2.3.4:5555" return cfg }, }, { args: []string{"-ui.addr", ":9998;cs=ui", "-proxy.cs", "cs=ui;type=file;cert=value"}, cfg: func(cfg *Config) *Config { cfg.UI.Listen.Addr = ":9998" cfg.UI.Listen.Proto = "https" cfg.UI.Listen.CertSource.Name = "ui" cfg.UI.Listen.CertSource.Type = "file" cfg.UI.Listen.CertSource.CertPath = "value" cfg.Registry.Consul.CheckScheme = "https" cfg.Registry.Consul.ServiceAddr = ":9998" return cfg }, }, { args: []string{"-ui.color", "value"}, cfg: func(cfg *Config) *Config { cfg.UI.Color = "value" return cfg }, }, { args: []string{"-ui.title", "value"}, cfg: func(cfg *Config) *Config { cfg.UI.Title = "value" return cfg }, }, { desc: "ignore aws.apigw.cert.cn", args: []string{"-aws.apigw.cert.cn", "value"}, cfg: func(cfg *Config) *Config { return cfg }, }, // config file { desc: "config from environ", environ: []string{"FABIO_proxy_addr=:6666"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":6666", Proto: "http"}} return cfg }, }, { desc: "config from url", args: []string{"-cfg", "URL"}, path: "http", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { desc: "config from file I", args: []string{"-cfg", "/tmp/fabio-config-test"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { desc: "config from file II", args: []string{"-cfg=/tmp/fabio-config-test"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { desc: "config from file III", args: []string{"-cfg='/tmp/fabio-config-test'"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, { desc: "config from file IV", args: []string{"-cfg=\"/tmp/fabio-config-test\""}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, // precedence rules { desc: "cmdline over config file I", args: []string{"-cfg", "/tmp/fabio-config-test", "-proxy.addr", ":6666"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":6666", Proto: "http"}} return cfg }, }, { desc: "cmdline over config file II", args: []string{"-proxy.addr", ":6666", "-cfg", "/tmp/fabio-config-test"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":6666", Proto: "http"}} return cfg }, }, { desc: "environ over config file", args: []string{"-cfg", "/tmp/fabio-config-test"}, environ: []string{"FABIO_proxy_addr=:6666"}, path: "/tmp/fabio-config-test", data: "proxy.addr = :5555", cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":6666", Proto: "http"}} return cfg }, }, { desc: "cmdline over environ", args: []string{"-proxy.addr", ":5555"}, environ: []string{"FABIO_proxy_addr=:6666"}, cfg: func(cfg *Config) *Config { cfg.Listen = []Listen{{Addr: ":5555", Proto: "http"}} return cfg }, }, // errors { desc: "-proxy.addr with unknown cert source 'foo'", args: []string{"-proxy.addr", ":5555;cs=foo"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("unknown certificate source \"foo\""), }, { desc: "-proxy.addr with unknown proto 'foo'", args: []string{"-proxy.addr", ":5555;proto=foo"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("unknown protocol \"foo\""), }, { desc: "-proxy.addr with proto 'https' requires cert source", args: []string{"-proxy.addr", ":5555;proto=https"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("proto 'https' requires cert source"), }, { desc: "-proxy.addr with proto 'grpcs' requires cert source", args: []string{"-proxy.addr", ":5555;proto=grpcs"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("proto 'grpcs' requires cert source"), }, { desc: "-proxy.addr with cert source and proto 'http' requires proto 'https', 'tcp', or 'grpcs'", args: []string{"-proxy.addr", ":5555;cs=name;proto=http", "-proxy.cs", "cs=name;type=path;cert=value"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'"), }, { desc: "-proxy.addr with cert source and proto 'tcp+sni' requires proto 'https', 'tcp' or 'grpcs'", args: []string{"-proxy.addr", ":5555;cs=name;proto=tcp+sni", "-proxy.cs", "cs=name;type=path;cert=value"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', 'prometheus', or 'grpcs'"), }, { desc: "-proxy.noroutestatus too small", args: []string{"-proxy.noroutestatus", "10"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("proxy.noroutestatus must be between 100 and 999"), }, { desc: "-proxy.noroutestatus too big", args: []string{"-proxy.noroutestatus", "1000"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("proxy.noroutestatus must be between 100 and 999"), }, { desc: "-proxy.auth with unknown auth type 'foo'", args: []string{"-proxy.auth", "name=myauth;type=foo"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("unknown auth type 'foo'"), }, { desc: "-proxy.auth with missing name", args: []string{"-proxy.auth", "type=basic;file=/some/file;realm=realm"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("missing 'name' in auth"), }, { desc: "-proxy.auth basic with missing file", args: []string{"-proxy.auth", "name=foo;type=basic;realm=realm"}, cfg: func(cfg *Config) *Config { return nil }, err: errors.New("missing 'file' in auth 'foo'"), }, { args: []string{"-glob.cache.size", "1000"}, cfg: func(cfg *Config) *Config { cfg.GlobCacheSize = 1000 return cfg }, }, { args: []string{"-cfg"}, cfg: func(cfg *Config) *Config { return nil }, err: errInvalidConfig, }, { args: []string{"-cfg=''"}, cfg: func(cfg *Config) *Config { return nil }, err: errInvalidConfig, }, { args: []string{"-cfg=\"\""}, cfg: func(cfg *Config) *Config { return nil }, err: errInvalidConfig, }, { desc: "valid bgp peers", args: []string{"-bgp.peers", "address=127.0.0.3;port=1179;asn=65000;" + "multihop=true;multihoplength=5;password=hunter2"}, cfg: func(cfg *Config) *Config { cfg.BGP.Peers = []BGPPeer{ { NeighborAddress: "127.0.0.3", NeighborPort: 1179, Asn: 65000, MultiHop: true, MultiHopLength: 5, Password: "hunter2", }, } return cfg }, }, } for _, tt := range tests { tt := tt // capture loop var if tt.desc == "" { tt.desc = strings.Join(tt.args, " ") } t.Run(tt.desc, func(t *testing.T) { // start a web server or write data to a file if tt.path is set switch { case tt.path == "http": srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, tt.data) })) defer srv.Close() // replace 'URL' with the actual server url in the command line args for i := range tt.args { tt.args[i] = strings.ReplaceAll(tt.args[i], "URL", srv.URL) } case tt.path != "": if err := os.WriteFile(tt.path, []byte(tt.data), 0600); err != nil { t.Fatalf("error writing file: %s", err) } defer os.Remove(tt.path) } // config parser expects the exe name to be the first argument cfg, err := Load(append([]string{"fabio"}, tt.args...), tt.environ) if got, want := err, tt.err; !reflect.DeepEqual(got, want) { t.Fatalf("got error %v want %v", got, want) } // limit the amount of code we have to write per test: // each test has a function which augments a pre-configured // config structure which is pre-filled with the defaults. clone := new(Config) *clone = *defaultConfig clone.Listen = []Listen{{Addr: ":9999", Proto: "http"}} got, want := cfg, tt.cfg(clone) verify.Values(t, "", got, want) }) } } ================================================ FILE: config/localip.go ================================================ package config import ( "log" "net" ) // LocalIP tries to determine a non-loopback address for the local machine func LocalIP() (net.IP, error) { addrs, err := net.InterfaceAddrs() if err != nil { return nil, err } for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() { if ipnet.IP.To4() != nil || ipnet.IP.To16() != nil { return ipnet.IP, nil } } } return nil, nil } func LocalIPString() string { ip, err := LocalIP() if err != nil { log.Print("[WARN] Error determining local ip address. ", err) return "" } if ip == nil { log.Print("[WARN] Could not determine local ip address") return "" } return ip.String() } ================================================ FILE: demo/aws/.gitignore ================================================ *.tfstate *.tfstate.backup ================================================ FILE: demo/aws/main.tf ================================================ # Specify the provider and access details provider "aws" { region = "${var.aws_region}" } # Create a VPC to launch our instances into resource "aws_vpc" "default" { cidr_block = "10.0.0.0/16" } # Create an internet gateway to give our subnet access to the outside world resource "aws_internet_gateway" "default" { vpc_id = "${aws_vpc.default.id}" } # Grant the VPC internet access on its main route table resource "aws_route" "internet_access" { route_table_id = "${aws_vpc.default.main_route_table_id}" destination_cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.default.id}" } # Create a subnet to launch our instances into resource "aws_subnet" "default" { vpc_id = "${aws_vpc.default.id}" cidr_block = "10.0.1.0/24" map_public_ip_on_launch = true } # A security group for the ELB so it is accessible via the web resource "aws_security_group" "elb" { name = "terraform_example_elb" description = "Used in the terraform" vpc_id = "${aws_vpc.default.id}" # HTTP access from anywhere ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # fabio admin access from anywhere ingress { from_port = 9998 to_port = 9998 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # outbound internet access egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } # Our default security group to access # the instances over SSH and HTTP resource "aws_security_group" "default" { name = "fabio_example" description = "Used in the terraform" vpc_id = "${aws_vpc.default.id}" # SSH access from anywhere ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTP access from the VPC ingress { from_port = 9999 to_port = 9999 protocol = "tcp" cidr_blocks = ["10.0.0.0/16"] } # HTTP access from the VPC ingress { from_port = 9998 to_port = 9998 protocol = "tcp" cidr_blocks = ["10.0.0.0/16"] } # outbound internet access egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_elb" "web" { name = "fabio-example-elb" subnets = ["${aws_subnet.default.id}"] security_groups = ["${aws_security_group.elb.id}"] instances = ["${aws_instance.web.id}"] listener { instance_port = 9999 instance_protocol = "http" lb_port = 80 lb_protocol = "http" } listener { instance_port = 9998 instance_protocol = "http" lb_port = 9998 lb_protocol = "http" } } resource "aws_proxy_protocol_policy" "ProxyProtocol" { load_balancer = "${aws_elb.web.name}" instance_ports = ["9999"] } resource "aws_key_pair" "auth" { key_name = "${var.key_name}" public_key = "${file(var.public_key_path)}" } resource "aws_instance" "web" { # The connection block tells our provisioner how to # communicate with the resource (instance) connection { # The default username for our AMI user = "ubuntu" # The connection will use the local SSH agent for authentication. } instance_type = "t2.micro" # Lookup the correct AMI based on the region # we specified ami = "${lookup(var.aws_amis, var.aws_region)}" # The name of our SSH keypair we created above. key_name = "${aws_key_pair.auth.id}" # Our Security group to allow HTTP and SSH access vpc_security_group_ids = ["${aws_security_group.default.id}"] # We're going to launch into the same subnet as our ELB. In a production # environment it's more common to have a separate private subnet for # backend instances. subnet_id = "${aws_subnet.default.id}" # We run a remote provisioner on the instance after creating it. # In this case, we just install nginx and start it. By default, # this should be on port 80 provisioner "remote-exec" { inline = [ "sudo apt-get -y update", "sudo apt-get -y install unzip", "wget https://releases.hashicorp.com/consul/0.6.4/consul_0.6.4_linux_amd64.zip", "wget https://github.com/fabiolb/fabio/releases/download/v1.1.2/fabio-1.1.2-go1.6.2_linux-amd64", "unzip consul*.zip" ] } } ================================================ FILE: demo/aws/outputs.tf ================================================ output "address" { value = "${aws_elb.web.dns_name}" } ================================================ FILE: demo/aws/variables.tf ================================================ variable "public_key_path" { default = "~/.ssh/terraform.pub" } variable "key_name" { default = "terraform" } variable "aws_region" { description = "AWS region to launch servers." default = "eu-west-1" } # Ubuntu Precise 14.04 LTS (x64) variable "aws_amis" { default = { eu-west-1 = "ami-f95ef58a" } } ================================================ FILE: docs/.gitignore ================================================ public/ ================================================ FILE: docs/README.md ================================================ # fabiolb.net website This is the source code for the https://fabiolb.net website. It is built with [Hugo](https://gohugo.io/) and automatically deployed to GitHub pages via Github Actions. The theme is [TheDocs](http://thetheme.io/thedocs/) from [TheTheme.io](http://thetheme.io/). This isn't a free theme and I've paid for it. Please don't just take it. Pay the designers. It isn't expensive. To render the page locally run the following command in the `fabio` root directory: $ hugo serve -s docs --disableFastRender To view the site open http://localhost:1313/ in your browser. ================================================ FILE: docs/archetypes/default.md ================================================ --- title: "{{ replace .TranslationBaseName "-" " " | title }}" date: {{ .Date }} draft: true --- ================================================ FILE: docs/config.toml ================================================ baseURL = "https://fabiolb.net/" languageCode = "en-us" title = "fabio - The Consul Load-Balancer" disableKinds = ["taxonomy"] ignoreLogs = ['warning-goldmark-raw-html'] [params] googleAnalytics = "UA-2812942-5" ================================================ FILE: docs/content/_index.md ================================================ --- title: "Overview" --- Fabio is an HTTP and TCP reverse proxy that configures itself with data from [Consul](https://consul.io/). Traditional load balancers and reverse proxies need to be configured with a config file. The configuration contains the hostnames and paths the proxy is forwarding to upstream services. This process can be automated with tools like [consul-template](https://github.com/hashicorp/consul-template) that generate config files and trigger a reload. Fabio works differently since it updates its routing table directly from the data stored in [Consul](https://consul.io/) as soon as there is a change and without restart or reloading. When you register a service in Consul all you need to add is a tag that announces the paths the upstream service accepts, e.g. `urlprefix-/user` or `urlprefix-/order` and fabio will do the rest. ### Maintainer Fabio was developed and maintained by Frank Schröder through January, 2020. Since that date primary maintenance has been the responsibility of [ENA](https://github.com/myENA) and the great community of users. It was originally developed at the [eBay Classifieds Group](https://www.ebayclassifiedsgroup.com/) in Amsterdam, The Netherlands. ================================================ FILE: docs/content/cfg/_index.md ================================================ --- title: "Config Language" weight: 550 --- The routing table is configured with commands in the language specified below. Rules are automatically generated by the configured backend. Additional rules can be stored in the Consul KV store configured by [`registry.consul.kvpath`](/ref/registry.consul.kvpath/) which by default is `/fabio/config`. As of fabio 1.5.7 the path is interpreted as a prefix and the values of all sub keys are appended in alphabetical order. When the [`log.routes.format`](/ref/log.routes.format/) is set to `all` then the routing table contains comments on the source of the fragment. ### Comments Blank lines and lines starting with `#` or `//` are ignored. ### `route add` Add a route for a service `svc` for the `src` (e.g. `/path` or `:port`) to a `dst` (e.g. `URL` or `host:port`). `route add [ weight ][ tags ",,..."][ opts "k1=v1 k2=v2 ..."]` Option | Description ------------------------------------------ | ----------- `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. `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. `strip=/path` | Forward `/path/to/file` as `/to/file` `prepend=/prefix` | Forward `/path/to/file` as `/prefix/path/to/file` `proto=tcp` | Upstream service is TCP, `dst` must be `:port` `pxyproto=true` | Enables PROXY protocol on outbount TCP connection `proto=https` | Upstream service is HTTPS `tlsskipverify=true` | Disable TLS cert validation for HTTPS upstream `host=name` | Set the `Host` header to `name`. If `name == 'dst'` then the `Host` header will be set to the registered upstream host name `register=name` | Register fabio as new service `name`. Useful for registering hostnames for host specific routes. `auth=name` | Specify an auth scheme to use (must be registered with the fabio server using `proxy.auth`) ##### Example ``` # route traffic for product-svc to 1.2.3.4:8000 and :9000 route add product-svc /product http://1.2.3.4:8000 route add product-svc /product http://1.2.3.4:9000 ``` ### `route del` Remove one or more routes which match the given criteria. ``` route del [ [ ]] route del tags ",,..." route del tags ",,..." ``` ##### Example ``` # remove all routes for 'product-svc' route del product-svc # remove all routes for 'product-svc' and /path route del product-svc /path # remove single route route del product-svc /path http://1.2.3.4:8000 # remove all routes for 'product-svc' matching a tag route del product-svc tags "green" # remove all routes matching a tag route del tags "yesterday" ``` ### `route weight` Directs a certain amount of traffic to instances matching certain criteria. The weight `w` is expressed as follows: * `w` is a float > 0 describing a percentage, e.g. 0.5 == 50% * `w <= 0`: means no fixed weighting. Traffic is evenly distributed * `w > 0`: route will receive n% of traffic. If sum(w) > 1 then w is normalized. * `sum(w) >= 1`: only matching services will receive traffic Note that the total sum of traffic sent to all matching routes is `w`%. The order of commands matters but routes are always ordered from most to least specific by prefix length. ``` route weight weight tags ",,..." route weight weight tags ",,..." route weight weight route weight service host/path weight w tags "tag1,tag2" ``` ##### Example ``` # Route 5% of the traffic to product-svc:/path with tags "green" route weight product-svc /path weight .05 tags "green" # Route 5% of the traffic to '/path' to the Go implementation. # Route the rest (95%) to the other implementation route weight /path weight .05 tags "lang=go" # Route 5% of the traffice to '/path' on the product-svc-go route weight product-svc-go /path weight .05 ``` ### Routing rules The routing table contains first all routes with a host sorted by prefix length in descending order and then all routes without a host again sorted by prefix length in descending order. For each incoming request the routing table is searched top to bottom for a matching route. A route matches if either `host/path` or - if there was no match - just `/path` matches. The matching route determines the target URL depending on the configured strategy. `rnd` and `rr` are available with `rnd` being the default. ##### Example The auto-generated routing table is ``` route add service-a www.mp.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-a www.kjca.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-a www.dba.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-b www.mp.dev/auth/ http://host-b:11080/ tags "a,b" route add service-b www.kjca.dev/auth/ http://host-b:11080/ tags "a,b" route add service-b www.dba.dev/auth/ http://host-b:11080/ tags "a,b" ``` The manual configuration under `/fabio/config` is ``` route del service-b www.dba.dev/auth/ route add service-c www.somedomain.com/ http://host-z:12345/ ``` The complete routing table then is ``` route add service-a www.mp.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-a www.kjca.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-a www.dba.dev/accounts/ http://host-a:11050/ tags "a,b" route add service-b www.mp.dev/auth/ http://host-b:11080/ tags "a,b" route add service-b www.kjca.dev/auth/ http://host-b:11080/ tags "a,b" route add service-c www.somedomain.com/ http://host-z:12345/ ``` ================================================ FILE: docs/content/code-of-conduct/_index.md ================================================ --- title: "Code of Conduct" weight: 1000 --- Be nice and treat others with respect. Please contact a project owner if you need mediation with a dispute. ================================================ FILE: docs/content/contact/_index.md ================================================ --- title: "Contact" weight: 2000 --- Please create a [GitHub](https://github.com/fabiolb/fabio/issues) issue for all feature requests, bugs and general questions about the project. You may also contact any project owner for other concerns. ================================================ FILE: docs/content/contrib/_index.md ================================================ --- title: "Contributing" weight: 900 --- Contributions to fabio of any kind are welcome including documentation, examples, feature requests, bug reports, discussions, helping with issues, etc. If you have a question on how or what to contribute just open an issue and indicate that it is a question. ================================================ FILE: docs/content/contrib/development.md ================================================ --- title: "Development" weight: 200 --- For newcomers to Go, you can't just `git clone` your forked repo and work from there, due to how Go's `GOPATH` works. You can follow the steps below to get started: 1. Fork this repository to your own account (named `myfork` below) 1. Make sure you have [Consul](https://www.consul.io/downloads.html) and [Vault](https://www.vaultproject.io/downloads.html) installed in your `$PATH` 1. `go get github.com/fabiolb/fabio`, change to the directory where the code was cloned (`$GOPATH/src/github.com/fabiolb/fabio`) and add your fork as remote: `git remote add myfork git@github.com:myfork/fabio.git` 1. Hack away! 1. `go fmt` and `make test` your code 1. Commit your changes and *push to your own fork*: `git push myfork` 1. Create a pull-request ================================================ FILE: docs/content/contrib/guidelines.md ================================================ --- title: "Guidelines" weight: 100 --- ### Your contribution is welcome! To make merging code as seamless as possible we ask for the following: * For small changes and bug fixes go ahead, fork the project, make your changes and send a pull request. Check out the [Development](/contrib/development/) page for some useful tips. * Larger changes should start with a proposal in an issue. This should ensure that the requested change is in line with the project and similar work is not already underway. * Only add libraries if they provide significant value. Consider copying the code (attribution) or writing it yourself. * Manage dependencies with `go mod` and run `go mod vendor` afterwards to sync the `vendor` folder for backwards compatibility. Once you are ready to send in a pull request, be sure to: * Sign the [CLA](https://cla-assistant.io/fabiolb/fabio) * Provide test cases for the critical code which test correctness. If your code is in a performance critical path make sure you have performed some real world measurements to ensure that performance is not degregated. * `go fmt` and `make test` your code * Squash your change into a single commit with the exception of additional libraries. * Write a good commit message. ================================================ FILE: docs/content/deploy/_index.md ================================================ --- title: "Deployment" weight: 300 --- The main use-case for fabio is to distribute incoming HTTP(S) and TCP requests from the internet to frontend (FE) services which can handle these requests. In this scenario the FE services then use the service discovery feature in [Consul](https://consul.io/) to find backend (BE) services they need in order to serve the request. That means that fabio is currently not used as an FE-BE or BE-BE router to route traffic among the services themselves since the service discovery of [Consul](https://consul.io/) already solves that problem. Having said that, there is nothing that inherently prevents fabio from being used that way. It just means that we are not doing it. ================================================ FILE: docs/content/deploy/amazon-api-gw.md ================================================ --- title: "Amazon API Gateway" weight: 500 --- You can deploy fabio as the target of an [Amazon API Gateway](https://aws.amazon.com/api-gateway/).
internet -- HTTP/HTTPS --> API GW -+- HTTP -> fabio -+-> service-b (host-b)
or behind an ELB with PROXY protocol support:
                                           +- HTTP w/PROXY -> fabio -+-> service-a (host-a)
                                           |                         |
internet -- HTTP/HTTPS --> API GW --> ELB -+- HTTP w/PROXY -> fabio -+-> service-b (host-b)
                                           |                         |
                                           +- HTTP w/PROXY -> fabio -+-> service-c (host-c)
You can authenticate calls from the API Gateway with a client certificate. This requires that you configure an HTTPS listener on fabio with a valid certificate.
internet -- HTTPS --> API GW -+- HTTPS w/client cert -> fabio -+-> service
To enable fabio to validate the Amazon generated certificate you need to configure the `aws.apigw.cert.cn` as follows: proxy.addr = 1.2.3.4:9999;your/cert.pem;your/key.pem;api-gw-cert.pem aws.apigw.cert.cn = ApiGateway `api-gw-cert.pem` is the certificate generated in the AWS Management Console. `your/cert.pem` and `your/key.pem` is 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. **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. ================================================ FILE: docs/content/deploy/amazon-elb.md ================================================ --- title: "Amazon ELB" weight: 400 --- You 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.
                                +- HTTP w/PROXY proto -> fabio -+-> service-a (host-a)
                                |                               |
internet -- HTTP/HTTPS --> ELB -+- HTTP w/PROXY proto -> fabio -+-> service-b (host-b)
                                |                               |
                                +- HTTP w/PROXY proto -> fabio -+-> service-c (host-c)
================================================ FILE: docs/content/deploy/direct.md ================================================ --- title: "Direct" weight: 100 --- In the following setup fabio is configured to listen on the public ip(s) where it can optionally terminate SSL traffic for one or more domains - one ip per domain.
                                           +--> service-a
                                           |
internet -- HTTP/HTTPS --> fabio -- HTTP --+--> service-b
                                           |
                                           +--> service-c
To scale fabio you can deploy it together with the frontend services which provides high-availability and distributes the network bandwidth.
           +- HTTP/HTTPS -> fabio -+- HTTP -> service-a (host-a)
           |                       |
internet --+- HTTP/HTTPS -> fabio -+- HTTP -> service-b (host-b)
           |                       |
           +- HTTP/HTTPS -> fabio -+- HTTP -> service-c (host-c)
================================================ FILE: docs/content/deploy/existing-lb.md ================================================ --- title: "Behind existing Gateway" weight: 200 --- In the following setup fabio is configured receive all incoming traffic from an existing gateway which also terminates SSL for one or more domains.
                                                          +--> service-a
                                                          |
internet -- HTTP/HTTPS --> LB -- HTTP --> fabio -- HTTP --+--> service-b
                                                          |
                                                          +--> service-c
Again, to scale fabio you can deploy it together with the frontend services which provides high-availability and distributes the network bandwidth.
                               +- HTTP -> fabio -+-> service-a (host-a)
                               |                 |
internet -- HTTP/HTTPS --> LB -+- HTTP -> fabio -+-> service-b (host-b)
                               |                 |
                               +- HTTP -> fabio -+-> service-c (host-c)
================================================ FILE: docs/content/faq/_index.md ================================================ --- title: "FAQ" weight: 800 --- ================================================ FILE: docs/content/faq/binding-to-low-ports.md ================================================ --- title: "Binding to Low Ports" --- If you want to bind fabio to ports below 1024 - so called privileged ports - without running fabio as `root` you can use an operating system approach as described below. These best practices are taken from https://github.com/fabiolb/fabio/issues/195. #### Linux Provide `net_bind_service` capability to fabio binary ``` $ setcap 'cap_net_bind_service=+ep' $(which fabio) ``` When using `systemd` you can use the following service definition: ``` $ cat /etc/systemd/system/fabio.service [Unit] Description=Fabio proxy After=syslog.target After=network.target [Service] LimitMEMLOCK=infinity LimitNOFILE=65535 Type=simple # unprivileged uid and gid User=fabio_user Group=fabio_group WorkingDirectory=/ ExecStart=/path/to/fabio -cfg /path/to/fabio.conf Restart=always # no need that fabio messes with /dev PrivateDevices=yes # dedicated /tmp PrivateTmp=yes # make /usr, /boot, /etc read only ProtectSystem=full # /home is not accessible at all ProtectHome=yes # to be able to bind port < 1024 AmbientCapabilities=CAP_NET_BIND_SERVICE NoNewPrivileges=yes # only ipv4, ipv6, unix socket and netlink networking is possible # netlink is necessary so that fabio can list available IPs on startup RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK ``` #### Solaris/Illumos/SmartOS Provide `net_privaddr` privileges to fabio user ```shell $ /usr/sbin/usermod -K defaultpriv=basic,net_privaddr fabio_user $ grep fabio_user /etc/user_attr fabio_user::::type=normal;defaultpriv=basic,net_privaddr ``` #### Provide privilege to fabio process (syntax needs review) ```shell $ /usr/sbin/ppriv -s PELI+NET_PRIVADDR -e fabio ``` #### OpenBSD/FreeBSD/NetBSD Use `PF` to forward from low port to high port. ``` /etc/pf.conf EXT_IF = "eth0" HTTPS_PORT = 443 HTTPS_PORT_BACKEND = 4343 LOCAL_IP = "127.0.0.1" ... pass in quick on $EXT_IF inet proto tcp from any to $LOCAL_IP port $HTTPS_PORT rdr-to $LOCAL_IP port $HTTPS_PORT_BACKEND ``` #### FreeBSD: Change the range of reserved ports (this looks dangerous) ```shell $ sysctl net.inet.ip.portrange.reservedhigh=79 # add to /etc/sysctl.conf to make this permament ``` #### macOS (needs review by SME) Use `launchd` to launch fabio by creating a service plist and using launchctl to run it: `$sudo launchctl load -w /path/to/fabio.plist` Example plist XML (needs reviewing): ```xml Label com.github.fabiolb.fabio Program fabio Sockets Listeners SockServiceName 80 SockType stream SockFamily IPv4 ``` #### Windows ??? ================================================ FILE: docs/content/faq/multiple-protocol-listeners.md ================================================ --- title: "Handling Multiple Protocols" --- It is quite possible for a single fabio instance to serve multiple protocols via distinct listeners. In this example: ``` proxy.addr = 172.16.20.11:80;proto=http;rt=60s;wt=30s,\ 172.16.20.11:443;proto=https;rt=60s;wt=30s;cs=all;tlsmin=10, \ 172.16.20.11:8443;proto=tcp+sni ``` We are telling fabio to bind to `172.16.20.11` on three different ports (`80`, `443`, and `8443`) using three distinct protocols (`HTTP`, `HTTPS`, `TCP+SNI`). You are free to bind to as many address, port, and protocol combinations as needed within a single instance. See [#490](https://github.com/fabiolb/fabio/issues/490) for context. ================================================ FILE: docs/content/faq/request-debugging.md ================================================ --- title: "Test fabio with curl" --- ##### How do I send a request to fabio via `curl`? ``` curl -v -H 'Host: foo.com' 'http://localhost:9999/path' ``` The `-x` or `--proxy` options will most likely not work as you expect as they send the full URL instead of just the request URI which usually does not match any route but the default one - if configured. ================================================ FILE: docs/content/faq/verifying-releases.md ================================================ --- title: "Verifying Releases" --- fabio releases can be verified by comparing the SHA256 checksum and by verifying the checksums with a GPG key. You can verify the SHA256 checksums with the GPG key below. You can also download it from most key servers using the ID For fabio release 5.14 and newer: [`76462AB9B0C185ABC66FD98F59861FC4870361CA`](https://keyserver.ubuntu.com/pks/lookup?search=76462AB9B0C185ABC66FD98F59861FC4870361CA&fingerprint=on&op=index) -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBF40mfQBEADHOlocoiOY66SLZtzJjCNKFeerYH2zHNU3sLK+sHp/76MUrPV4 uDG3T6a6QK0HUKLy/hxKh/wftNCOaSYTwNVbYJ1EYBnBEgxuKNM8K5xOCKjwWrXF J80xoXBJXXmJvOFHEoWjUnDAMVUJyf3bt0sT0vOA5OTdbd2LhimDOpeIiO/umZKp 0ZsDcjUPUuIenqnKyk4UwAfXdWxrj2g5/But1n3nasvgtEtQg9CaSloh6Zgzcy+3 I+jpCn2FLOay+THABkM+XmjSYudkIFlqsZwkB2GxwTaRXENt8QUK7i4GWVCcPN6x gYgIz9uLZQXkkxGZvasC5fUm/W6F0pyz1wUbbizhDuBhoez3XdJdhW8nWCT6rg5M ejgkSVoG/fqoG9SoFXeTlQjZJSc8+0pTgWsqnuwmM+eFllvORSKS7uwBNg7jvFPv 4yLGCR5bGxTX7VM4XPkLR2pUF/nHmSohiGOWpqw+PRVwOWBBMi+r4c4SckR4MMOB NK+KJTQnildsnqw/mvf98Op4GrAtD4MQDFRKD2TSIq60qFTe6MF77P50Z33r6x5x CPN7XYTzZKPPiHf5uWtyOvH3V+vxHX2N0zAsRADXW+Jsly/Wwt0k8Km3beaF/Jvw AFQwneh50L5Pv+Tb+8b6xS8gvIeGPgs4lcxRDxFEcFKN58OjtDelN212cQARAQAB tCVhZG1pbkBmYWJpb2xiLm5ldCA8YWRtaW5AZmFiaW9sYi5uZXQ+iQJUBBMBCgA+ FiEEdkYqubDBhavGb9mPWYYfxIcDYcoFAl40mfQCGwMFCQk/xgAFCwkIBwMFFQoJ CAsFFgIDAQACHgECF4AACgkQWYYfxIcDYcqCgQ/8CfH2EBmBlHB7jlI4nFu17fqV WTXxhuo2UcTCQ3G8at32V27FZTFq64rtY7/QmY3HyHhdn77NXzIlLDsaD07IEBpw GFf05V1vVm/Y+DB/3vmHr+bEP5bB4RZqYz+U1cSGTEg2S3sOuz416gJdoCFN8Lin 1fHRuGfZTJ2j2oQhUsYbt+GBpPm7xtpqK4yfCd4gT2vhDzbDG9QSLMrrLh/aA6Ya IcZZCsXpnRPhfvPrp0LuIY9Lml+EaMfNxsoXYl2W5c+BpXG93ThSLKPc8XM/7e4A CkRWNLKihVZNDmGCIy2FIFIV9YlEIhAAtZPhsUE3rnrIUgHETPYwDvAJB4pbJrLe bwnRuWZlYNsPZp8W4RxbQVcHpsg+sWoyAkykWxs9FbxgXEGd0+wP5tumFquyfijg eQLnsFU7KlQA+5Rh6ulrvzMNHFBYLoPa+U1soR6Jg0hCPhkzc+6tzTrmUCg7H7+i 49szuN2KZr6k5GR+f2p9mOlnHmjJSJVULtnBQJMfTEnqzszvw9OgO1j72x7hTVRO UQSV6NXr0GFr293iTJS1x2/zFETCZelxVwbyp0t/psDz8nv6aXMcSjzcoWgmRRcP zpfNidLLp3Ym9XKtz7kvPI/PRTsHoO+qw6H6Kw8jMxxIv5hApCI/YOt5GFBlXmZq hBckyt1rS0kW5zQsStS5Ag0EXjSZ9AEQAMrim1LXqnqdMJlc6sj++TZgoLeYmtSI 4n/J1AGk9/BIumJKgCL5TPvUhz7HUWjhOqhtH/1/EyxPTI25Up7QcQKb0TYG/6Gn 3mIeBsvTdPZWmwq0e7aCrTSU8bYNnuMKAFxlPPG/lu7v1QQkaPgbEMOZI7cDA7V8 TLs/uQcAjGPdu2f2mJ/m+kgjeOwud+43CF4aI2/eVd39DqjjDrRImUc3OXypE4vW PRq2ooSnS7VE0yU3QBubdPB8Y7x7R5bDE9fgLjZ9t//bSLgZfVzZoc7TvycH9opk zr1LD4XEdZYFWc1h7++ci+f75/QQppPto3ItK61oUnpyO5J0Bl/Ay7086xU8b5Be mPFDVMUE8SW2a+baaDKwbYUvImSI2CwNkCuYieGuAueMkY+Coe7AdaDhtuzINkby e9ALGGbpRi/ByURQoW9akQt+ap7I8/bdp+IFYWT8K1HFogd5y0+TYaatpnT9jJYM 64GtnDhyD2ncyLNM1a7YOn4e+WWiK8datzn962VsaSXjAPKvVROkgLoedDU9oiDm ITDZgcsyY6ATgYmzlN2Qm8ubig1adZdGWsWzv0d9Qj8AEzsPqRVrQ6Ofc/sNi5Y3 ELSOpWUOetbKEBFYe3oA2Bu6LOqd3lcKittWke3RMkehKFqxFdmBwjcrCtIjLicv IemWK6rAAYmJABEBAAGJAjwEGAEKACYWIQR2Riq5sMGFq8Zv2Y9Zhh/EhwNhygUC XjSZ9AIbDAUJCT/GAAAKCRBZhh/EhwNhyrzoEACe9SVpr6TaFvIcfcvj9d4FOmiK Tgm64SEnYDDs6JhzD3p38Ut80d6y2vg9WUMUA3dhftbAyr/rqkZghiV3UhWJGPJm AGWVG3p5TpSPCloFUlHHMWXCJm4UAoo75ud15PYD8CtUfOYc68A7a+9f+1dC5gRy rVjBltWshsai+CjksRlg64wGMvJL7ghcsGoxFOzU/khGvo5JZ3OzObscYLxBKPnY sUPerHnKB63CYxNfkd2aziapE7zXqoN1ZAFKwsBp38CiuBIT+8bb6+vAy9azfW/J mGqjn4vfBUpdTsPbRRRI3CAoUN8R5QqVCCzV6hcv2p921ZWNpO0QxaHJYq0W3mwH ls5eJOWJwx3qZ8ZB84fnuUb1YhzNjOSJDjgE8ZJ1iHf+ZTpqNRNbsyshfPcI5FYR /PKPXTGNTeTFAXiQ/UjxFK/UEVWs3mDfqtyvC+Z5s7jCGabPwoOvWeHGMHUWWZRv NU+TL+pUMWY29wKsDsk7zriokCDApNnJJb52/tIzk/XHMLPBjGSoYinKYMALYbAp 6UvSeJ6cJ/+5vwXJadMyiYrsPPQiuVCUfVg6KcX6B/+2MaKoyY3s8DaZ1vFdtZcg 1tjLI383GOEuDGfUDOgrlTikgpxbT2q4Zq80aQhPD8mMlpqdTO4UWfvwwx0FPH04 5xVKlvTztaHhtaWHkg== =b3Un -----END PGP PUBLIC KEY BLOCK----- For release 5.13 and older: [`D8B19A29317E92E470D7CD67021E03CADDA53977`](http://pgp.key-server.io/search/0xD8B19A29317E92E470D7CD67021E03CADDA53977) -----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 mQENBFHXufMBCADO35ztkc+e22Oyfxa7npmqljgZs4O3qFB3YBY0AiFqZ+YDwc1P 2sb9r76M6J9sMiijFHZ4NZkHm1NOPgiEK13fLc/cDlrDMbbv7yqrBlYZuaQxPvCw Bv+zAyVyNqy79sbQpXId7bokAMthrAf69x9F1/HaBmqspi6/8JWcQmNcGVqaABRk eQSB/Oq8DYBawroMRUGNtyTMKJ5FAbsYeDH7kiOlBtJxaxdhzlMX/4W6PUVXCOF+ 44CKVWl7eIwXkdbkAVOy2AgqG6b+X9svbjNvV0GFErozHwCjIxSKT2m/jTkey4oq st9eBuNClEKtduxjCzkbhLX+Xvqg9vPNCY1NABEBAAG0K0ZyYW5rIFNjaHJvZWRl ciA8ZnJhbmsuc2Nocm9lZGVyQGdtYWlsLmNvbT6JAT4EEwECACgCGy8GCwkIBwMC BhUIAgkKCwQWAgMBAh4BAheABQJX1Z3mBQkJwErzAAoJEAIeA8rdpTl3MucIAIqx 0qPeNCiT0EnJfMNaI0ttx/+Y+hF/35XqXbuhAXDPUSwqyNAt+6qdKwnc7J4ZVZx6 rdH0jUoNbXoN/y/QUsmtktiQqmnyFaAT3CUphg5ZcB6g+/RUPJ0uyXY+UgB7LhLd tyYyxJamfhpf0O+IEVQ+MqTvI6glCoN0s7LGGJR+/E5xbrJv8VdGrHFSPe6i4nU9 axz38MzEkHPDYUcd+6QaYN82tiuL+ipkHudOOs4aO02x18g6cg7BBZFKrAPLP7SX TTG94mRhf9OEeKc/gTrHqQ+ZBrwyDZKS13LHoHYLkRVIyWDl3t3SU/U+TGsroR4a dGQoe4tJPzZ3X5hAlK20JkZyYW5rIFNjaHJvZWRlciA8ZnJzY2hyb2VkZXJAZWJh eS5jb20+iQE+BBMBAgAoAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCV9Wd 5gUJCcBK8wAKCRACHgPK3aU5d5hFCACe+JXpMRfqsfvc8rc4euxg0GSQGI8XfBx+ qziCO37cPTtAxyQGg5Z/ZXe6xv1kDP73mUkpZ9DLBlbCdYtC/l1LG+cYV8f/sa9K FTn924j86R5ABqwgyo2ACE5iDFOA52ud2ZqVrjjOfqzShQZGanM+X+9A+5NHO7ZD RG2LqR+b9VG8bKIhbCddu6q0/CB722PSqCVo4tAZ4W6oiA2D6QB/GfMUswSntN7e nhyjEWM6701Kk5hcTrsAIEMPRLwz+NwEb63cJ5XNsIl6vIsBkGtuxTSz/2/ecwlp hh4XWLTG+I+AkEo4mUUCMdieRf+IGjXXnogmJyfGtE2BTraO+v48tC1GcmFuayBT Y2hyb2VkZXIgPGZyYW5rLnNjaHJvZWRlckBnby1sZWZ0LmNvbT6JAT4EEwECACgC Gy8GCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheABQJX1Z3mBQkJwErzAAoJEAIeA8rd pTl3qqkH/2Jmm44cZNDAmleoOq/gOBdDUE1Ot8wiNOnaa08Eem9DyRJ312TDXUTu gyCyNFfimln4G+XYRqw6r1Vxxtv7VMZmAaVd2q6aAC+SEbAYo32mNU5cVF1FTu04 VTUlkV5jkG1mHBOVs3yxkxd+YPZQsXD3GaaFhd6/NCpqrAxzkGcEZvoHWz8vGTlf RxX5qnQeLg8VxttjSuXpfHbYptAwYmX+nDC6lL9IriJhRFtH3dVed2iHIiYtxlF6 7enUv6PndL1GHHXNzl5CFtJaG2Tr8NgdCTycymLAonXvm8j4zE1jRihnZ4TS+6Zk UYS6G01WlBeHLEvCCxt8vpzKQFgg0kS5AQ0EUde58wEIAMgdQBVvrzrIJmN6i5B0 6Ey7bUBUijqiAQ5ful+aKANEePdshgapphfJstRAz+ppjZsBwN2XKFtVbY5znzDZ dukvqB8A9KXOt0a4TT/q9JPE2ivlhp2ldhAr8LUcBNSLNTKKR5HwOSrMVxTaP6Pq ZwbeW6NuDsPCLk0YpkcT13YrDK29IObZMr6g7UkZ3UD0w99OveQdimMvslIwd/sp wP9oRefqZ1vhyllOA0SFmSlzTk7iSLrBzoGAXALhGpUdxN1LsCIvDZGydm7YtOGX GOu+VTA6odHOB5urEEGn+dMPlF1qEcqirvYFmnamcnjrhn2nbxLn8jz2MRlNTGPt 88sAEQEAAYkCRAQYAQoADwIbLgUCV9WeGQUJCcBLJgEpwF0gBBkBCgAGBQJR17nz AAoJEE1lxursh97NIVsH/i1edqJvxH2naj49hMp3m8OSUj0cOkTA1rebeF33YFLn XqHUdL1DelFQIZ8zXcS5E5iB7OceqxNjoJaGU0k4yXg4IU52xtHcwM2FqxtRNMds 4yb+Hpopm2oLl9lsnA/Rm9pqNGoVN6Hc/mbueYpVxB1jKFqH1mX3+G/h5Z6YzPXg jf6F8SLgM1kfJA7zb77Gghe5+xtYNGqoqRFne3YqHApXYfTbOFxr+5+32v2m7ib/ OI4p5Zq/y/F5+QLn+phGsWeYrmGCalyTzxCZvDgtgDsucqF55G8EIxiPQ/IQrs+y /VuL+nvIZjKJO8X8r0AXNk7HA/KTxTUkRYArAwLj+skJEAIeA8rdpTl3nnYH/1z6 tNBVVDRl/jU9m0yj4PpNMZUjd+t0jH3WzMrqKbN7/Io8kFCLJdmg4+97tXjVtRbR CWw7K43cOpQchXh3t+cwtcfdJd6TOqJCO21laQL0CBIZlNS9lQ7c4J9eew3MKe4Y D22kOes/SXAIONU/KNP+aLXy8iMvXcxKe3vsZj4g4Huk4+mXkoFIrVit/tm7PIqm VZd8lFRK841qZdL+vc/JQR8b8o3CtTbBJ4KO/sylw0M0EHsIQv2TL//NABiCuob/ gjpASGi/ZqGVKl/twu9ZaL3Z07uOGOlAlvU5VVlA/1gf39BoUA1ZL5loPNlERPIF oqPZ3VIfhRjqQbvOnDU= =OeUJ -----END PGP PUBLIC KEY BLOCK----- ## Checksum Verification If you would like to verify the checksum of a fabio download, please note that only the `sha256` file is signed by the GPG key below. The binaries themselves are not signed, but rather hashed. To verify the integrity of a particular binary: Download the binary, `sha256`, and `sha256.sig` files Verify the `sha256` file is properly signed Verify the `sha256` in the file matches the binary For example: ``` # This is the public key from above - one-time step. gpg --import fabiolb.asc # Download the binary and signature files. curl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15-linux_amd64 curl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15.sha256 curl -OsL https://github.com/fabiolb/fabio/releases/download/v1.5.14/fabio-1.5.14-go1.15.sha256.sig # Verify the signature file is untampered. gpg --verify fabio-1.5.14-go1.15.sha256.sig fabio-1.5.14-go1.15.sha256 # Verify the SHASUM matches the binary. grep linux_amd64 fabio-1.5.14-go1.15.sha256 | shasum -a 256 -c - ``` ## Note Parts of this text have been taken from the [HashiCorp Security Page](https://www.hashicorp.com/security.html) which describes the procedure which is also used for fabio. ================================================ FILE: docs/content/faq/why-fabio.md ================================================ --- title: "Why 'Fabio'?" --- When I was writing fabio my son was watching "Finding Nemo" almost every day and Dory keeps getting Nemos' name wrong. One of the names she called him was "Fabio". Hence the name. So no, this isn't about the first male super-model ... ================================================ FILE: docs/content/feature/_index.md ================================================ --- title: "Features" weight: 200 --- The following list provides a list of features supported by fabio. * [Access Logging](/feature/access-logging/) - customizable access logs * [Access Control](/feature/access-control/) - route specific access control * [Certificate Stores](/feature/certificate-stores/) - dynamic certificate stores like file system, HTTP server, [Consul](https://consul.io/) and [Vault](https://vaultproject.io/) * [Compression](/feature/http-compression/) - GZIP compression for HTTP responses * [Docker Support](/feature/docker/) - Official Docker image, Registrator and Docker Compose example * [Dynamic Reloading](/feature/dynamic-reloading/) - hot reloading of the routing table without downtime * [Graceful Shutdown](/feature/graceful-shutdown/) - wait until requests have completed before shutting down * [HTTP Header Support](/feature/http-headers/) - inject some HTTP headers into upstream requests * [HTTPS Upstreams](/feature/https-upstream/) - forward requests to HTTPS upstream servers * [Metrics Support](/feature/metrics/) - support for Graphite, StatsD/DataDog and Circonus * [PROXY Protocol Support](/feature/proxy-protocol/) - support for HA Proxy PROXY protocol for inbound requests (use for Amazon ELB) * [Path Stripping](/feature/http-path-stripping/) - strip prefix paths from incoming requests * [Path Prepending](/feature/http-path-prepending/) - prepend a prefix path on to incoming requests * [Server-Sent Events/SSE](/feature/sse/) - support for Server-Sent Events/SSE * [TCP Proxy Support](/feature/tcp-proxy/) - raw TCP proxy support * [TCP-SNI Proxy Support](/feature/tcp-sni-proxy/) - forward TLS connections based on hostname without re-encryption * [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 * [Traffic Shaping](/feature/traffic-shaping/) - forward N% of traffic upstream without knowing the number of instances * [Web UI](/feature/web-ui/) - web ui to examine the current routing table * [Websocket Support](/feature/websockets/) - websocket support * [BGP Support](/feature/bgp) - bgp support ================================================ FILE: docs/content/feature/access-control.md ================================================ --- title: "Access Control" since: "1.5.8" --- fabio supports basic ip centric access control per route. You may specify one of `allow` or `deny` options per route to control access. Currently only source ip control is available. To allow access to a route from clients within the `192.168.1.0/24` and `fe80::/10` subnet you would add the following option: ``` allow=ip:192.168.1.0/24,ip:fe80::/10 ``` With this specified only clients sourced from those two subnets will be allowed. All other requests to that route will be denied. Inversely, to deny a specific set of clients you can use the following option syntax: ``` deny=ip:fe80::1234,100.123.0.0/16 ``` With this configuration access will be denied to any clients with the `fe80::1234` address or coming from the `100.123.0.0/16` network. Single host addresses (addresses without a prefix) will have a `/32` prefix, for IPv4, or a `/128` prefix, for IPv6, added automatically. That means `1.2.3.4` is equivalent to `1.2.3.4/32` and `fe80::1234` is equivalent to `fe80::1234/128` when specifying address blocks for `allow` or `deny` rules. The source ip used for validation against the defined ruleset is taken from information available in the request. For `HTTP` requests the client `RemoteAddr` is always validated followed by all elements of the `X-Forwarded-For` header, if present. When all of these elements match an `allow` the request will be allowed; similarly when any element matches a `deny` the request will be denied. For `TCP` requests the source address of the network socket is used as the sole paramater for validation. If the inbound connection uses the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to transmit the true source address of the client then it will be used for both `HTTP` and `TCP` connections for validating access. ================================================ FILE: docs/content/feature/access-logging.md ================================================ --- title: "Access Logging" since: "1.4.1" --- Support for writing access logs for HTTP requests in the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) or the [Combined Log Format](https://httpd.apache.org/docs/2.4/logs.html#combined) or a custom format to stdout. By default, access logs are disabled. To enable them set `log.access.target=stdout`. This will write access logs in the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) to stdout. The standard fabio logs are still written to stderr. The log format can be controlled with the `log.access.format` parameter which is either `common`, `combined` - which outputs the [Combined Log Format](https://httpd.apache.org/docs/2.4/logs.html#combined) - or a custom format string which is fully described in [fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties#L374-L421). ``` # log.access.format configures the format of the access log. # # If the value is either 'common' or 'combined' then the logs are written in # the Common Log Format or the Combined Log Format as defined below: # # 'common': $remote_host - - [$time_common] "$request" $response_status $response_body_size # 'combined': $remote_host - - [$time_common] "$request" $response_status $response_body_size "$header.Referer" "$header.User-Agent" # # Otherwise, the value is interpreted as a custom log format which is defined # with the following parameters. Providing an empty format when logging is # enabled is an error. To disable access logging leave the log.access.target # value empty. # # $header. - request http header (name: [a-zA-Z0-9-]+) # $remote_addr - host:port of remote client # $remote_host - host of remote client # $remote_port - port of remote client # $request - request # $request_args - request query parameters # $request_host - request host header (aka server name) # $request_method - request method # $request_scheme - request scheme # $request_uri - request URI # $request_url - request URL # $request_proto - request protocol # $response_body_size - response body size in bytes # $response_status - response status code # $response_time_ms - response time in S.sss format # $response_time_us - response time in S.ssssss format # $response_time_ns - response time in S.sssssssss format # $time_rfc3339 - log timestamp in YYYY-MM-DDTHH:MM:SSZ format # $time_rfc3339_ms - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format # $time_rfc3339_us - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format # $time_rfc3339_ns - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format # $time_unix_ms - log timestamp in unix epoch ms # $time_unix_us - log timestamp in unix epoch us # $time_unix_ns - log timestamp in unix epoch ns # $time_common - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ # $upstream_addr - host:port of upstream server # $upstream_host - host of upstream server # $upstream_port - port of upstream server # $upstream_request_scheme - upstream request scheme # $upstream_request_uri - upstream request URI # $upstream_request_url - upstream request URL # $upstream_service - name of the upstream service # # The default is # # log.access.format = common ``` ================================================ FILE: docs/content/feature/authorization.md ================================================ --- title: "Authorization" since: "1.5.11" --- fabio supports basic http authorization on a per-route basis. Authorization schemes are configured with the `proxy.auth` option. You can configure one or multiple schemes. Each authorization scheme is configured with a list of key/value options. name=;type=;opt=arg;opt[=arg];... Each scheme must have a **unique name** which is then referenced in a route configuration. proxy.auth = name=myauth;type=... When you configure the route, you must reference the unique name for the authorization scheme: route add svc / https://127.0.0.1:8080 auth= urlprefix-/ proto=https auth= The following types of authorization schemes are available: * [`basic`](#basic): legacy store for a single TLS and a set of client auth certificates At the end you also find a list of [examples](#examples). ### Basic The 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. The `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. Note: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored. name=;type=basic;file=;realm=;refresh= Supported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd) #### Examples # single basic auth scheme name=mybasicauth;type=basic;file=p/creds.htpasswd; # single basic auth scheme with refresh interval set to 30 seconds name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s # basic auth with multiple schemes proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s, name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm ================================================ FILE: docs/content/feature/bgp.md ================================================ --- title: "BGP" since: "1.6.3" --- NOTE: This feature does not work on Windows at present since the gobgp project does not support windows. This feature integrates the functionality of [gobgpd](https://github.com/osrg/gobgp) with fabio. This is particularly useful in the scenario where we are using anycast IP addresses and want to dynamically advertise to upstream routers when we're ready to receive traffic. In the past, we've used external router packages such as quagga or frr to handle this for us, but it's potentially messy to make sure that the route advertisement stops if fabio goes down, and the bgp daemon is started back up once fabio is running again. By integrating the bgp advertisement with the proxy server, we've made sure that when fabio goes down, the route is no longer advertised and traffic can be sent to other fabio instances accordingly. When fabio is back up, the route is advertised again. Further, the gobgp [command line client](https://github.com/osrg/gobgp/blob/master/docs/sources/cli-command-syntax.md) is fully supported by enabling the [bgp.enablegrpc](/ref/bgp.enablegrpc/) option. [Multihop](/ref/bgp.peers/) is supported, where fabio may not be on the same subnet as neighbor. To enable BGP, you must at a minimum: * Set up an anycast interface on the host with a /32 address. On linux, the dummy interface type is a good option since it's supported using network manager. Another option is hanging this address off of loopback. * Configure the neighbor / peer / upstream router to allow us to peer, and to allow our anycast as a prefix it will accept * Set [bgp.enabled](/ref/bgp.enabled/)=true * Configure the [bgp.asn](/ref/bgp.asn/) to be our router's Asn - probably use a private ASN here * Configure the [bgp.routerid](/ref/bgp.routerid/) to be our router's IP address (i.e., not the anycast address, something unique). This will be the default nexthop of all routes we publish. * Configure the [bgp.peers](/ref/bgp.peers/) for at least one nieghbor. * Configure the [bgp.anycastaddresses](/ref/bgp.anycastaddresses) for at least one anycast address. This will embed a gobgpd instance inside of fabio on startup and it will publish the configure anycast addresses. It will also configure a [gobgpd policy](https://github.com/osrg/gobgp/blob/master/docs/sources/policy.md) that will reject all incoming prefixes from neighbors. Alternatively, for more advanced use cases, you can reference an [external gobgpd config file](/ref/bgp.gobgpdcfgfile/) that will override many of the options set in the fabio config, including the policy blocking us from accepting prefixes from neighbors. You still need to specify the bgp.grpc options from the fabio config since there is no analog in the gobgpd config file. You may still specify bgp anycastaddresses or bgp.peers from the fabio config, but we ignore anything that would be specified in the global section of the gobgpd config file, including router ID and the ASN. Even If the bgp.gobgpdcfgfile value is set, fabio will still honor any values configured for bgp.anycastaddresses or bgp.peers. These will be processed after the config file is processed. ### Note For situations where multiple fabio instances are running with the same anycast address in the same datacenter, or in any other situation where the path distance is the same and load balancing across multiple fabio instances is desired, the details of ECMP configuration is outside the scope of this document as configuration would vary greatly depending on the details of the upstream router. ================================================ FILE: docs/content/feature/certificate-stores.md ================================================ --- title: "Certificate Stores" since: "1.2" --- Support for dynamic certificate stores which allow you to store certificates in a central place and update them at runtime or generate them on the fly without restart. You can store certificates in files, directories, on HTTP servers in [Consul](https://consul.io/) or in [Vault](https://vaultproject.io/). You can use [Vault](https://vaultproject.io/) to generate certificates on the fly. Starting with version 1.2 fabio has support for dynamic certificate stores which allow you to store certificates in a central place and update them at runtime without restarting fabio. As of Go <= 1.7 only TLS certificates can be changed at runtime. For updating client auth certificates [golang issue 16066](https://github.com/golang/go/issues/16066) is open. Certificate stores are configured with the `proxy.cs` option. You can configure one or multiple stores. Each certificate source is configured with a list of key/value options. cs=;type=;opt=arg;opt[=arg];... Each source must have a **unique name** which is then referenced in a listener configuration. proxy.cs = cs=mycerts;type=... proxy.addr = 1.2.3.4:9999;cs=mycerts;... All certificates must be provided in **PEM format** The following types of certificate sources are available: * [`file`](#file): legacy store for a single TLS and a set of client auth certificates * [`path`](#path): load certificates from a directory (e.g. managed by puppet/chef/ansible/...) * [`http`](#http): load certificates from an HTTP server * [`consul`](#consul) : load certificates from [Consul](https://consul.io/) KV store * [`vault`](#vault) : load certificates from [Vault](https://vaultproject.io/) All certificate stores offer a set of [common options](#common-options). If you want to use client certificate authentication with an Amazon API gateway check the `caupgcn` option there. At the end you also find a list of [examples](#examples). ### File The file certificate source supports one certificate which is loaded at startup and is cached until the service exits. The `cert` option contains the path to the certificate file. The `key` option contains the path to the private key file. If the certificate file contains both the certificate and the private key the `key` option can be omitted. The `clientca` option contains the path to one or more client authentication certificates. ##### Example cs=;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem ### Path The path certificate source loads certificates from a directory in alphabetical order and refreshes them periodically. The `cert` option provides the path to the TLS certificates and the `clientca` option provides the path to the certificates for client authentication. TLS certificates are stored either in one or two files: www.example.com.pem or www.example.com-{cert,key}.pem TLS certificates are loaded in alphabetical order and the first certificate is the default for clients which do not support SNI. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. The default refresh interval is 3 seconds and cannot be lower than 1 second to prevent busy loops. To load the certificates only once and disable automatic refreshing set `refresh` to zero. ##### Example cs=;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s ### HTTP The http certificate source loads certificates from an HTTP/HTTPS server. The `cert` option provides a URL to a text file which contains all files that should be loaded from this directory. The filenames follow the same rules as for the path source. The text file can be generated with: ls -1 *.pem > list The `clientca` option provides a URL for the client authentication certificates analogous to the `cert` option. Authentication credentials can be provided in the URL as request parameter, as basic authentication parameters or through a header. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. The default refresh interval is 3 seconds and cannot be lower than 1 second to prevent busy loops. To load the certificates only once and disable automatic refreshing set `refresh` to zero. ##### Example cs=;type=http;cert=https://host.com/path/to/cert/list&token=123 cs=;type=http;cert=https://user:pass@host.com/path/to/cert/list cs=;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234 ### Consul The consul certificate source loads certificates from [Consul](https://consul.io/). The `cert` option provides a KV store URL where the the TLS certificates are stored. The `clientca` option provides a URL to a path in the KV store where the the client authentication certificates are stored. The filenames follow the same rules as for the [`path`](#path) source. The TLS certificates are updated automatically whenever the KV store changes. The client authentication certificates cannot be updated automatically since Go does not provide a mechanism for that yet. (See [golang issue 16066](https://github.com/golang/go/issues/16066)) ##### Example cs=;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123 ### Vault The Vault certificate store uses HashiCorp Vault as the certificate store. The `cert` option provides the path to the TLS certificates and the `clientca` option provides the path to the certificates for client authentication. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. Certificate 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. Example: ``` vault write secret/fabio/certs/www.domain.com cert=@cert.pem key=@key.pem ``` The path to vault must be provided in the `VAULT_ADDR` environment variable. The token must be provided in the `VAULT_TOKEN` environment variable. **fabio versions <= 1.2.1** require a token with root and/or sudo privileges to create an orphan token for itself. This required fabio to have more privileges than it needs and it also prevented revoking the fabio token if the parent token was revoked. Therefore, supplying a token with root and/or sudo privileges is now deprecated and will be removed in a later release. **fabio versions > 1.2.1** will no longer attempt to create a token itself and instead solely rely on the provided token. The provided token can be an orphan and should be renewable for the duration fabio is expected to run. It is recommended not to set the `explicit_max_ttl` unless fabio is restarted before that time expires. fabio needs the following policies set on the path where the certificates are stored, for example: # For Vault < 0.7 path "secret/fabio/cert" { capabilities = ["list"] } # For Vault >= 0.7; note the trailing slash path "secret/fabio/cert/" { capabilities = ["list"] } path "secret/fabio/cert/*" { capabilities = ["read"] } ##### Example cs=;type=vault;cert=secret/fabio/certs ### Common options All certificate stores support the following options: * `caupgcn` : Upgrade a self-signed client auth certificate with this common-name to a CA certificate. Typically used for self-singed certificates for the Amazon AWS API Gateway certificates which do not have the CA flag set which makes them unsuitable for client certificate authentication in Go. For the AWS API Gateway set this value to 'ApiGateway' to allow client certificate authentication. This replaces the deprecated parameter 'aws.apigw.cert.cn' which was introduced in version 1.1.5. ### Examples # file based certificate source proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem # path based certificate source proxy.cs = cs=some-name;type=path;cert=path/to/certs # HTTP certificate source proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs # Consul certificate source proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123 # Vault certificate source proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs # Multiple certificate sources proxy.cs = cs=srcA;type=path;cert=path/to/certs,\ cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs # path based certificate source for AWS Api Gateway proxy.cs = cs=some-name;type=path;cert=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway ================================================ FILE: docs/content/feature/docker.md ================================================ --- title: "Docker Support" since: "1.0" --- To run fabio within Docker use the official Docker image `fabiolb/fabio` and mount your own config file to `/etc/fabio/fabio.properties` docker run -d -p 9999:9999 -p 9998:9998 -v $PWD/fabio/fabio.properties:/etc/fabio/fabio.properties fabiolb/fabio If you want to run the Docker image with one or more SSL certificates then you can store your configuration and certificates in `/etc/fabio` and mount the entire directory, e.g. $ cat ~/fabio/fabio.properties proxy.addr=:443;/etc/fabio/ssl/mycert.pem;/etc/fabio/ssl/mykey.pem docker run -d -p 443:443 -p 9998:9998 -v $PWD/fabio:/etc/fabio fabiolb/fabio The official Docker image contains the root CA certificates from a recent and updated Ubuntu 12.04.5 LTS installation. ### Registrator If you use Gliderlabs [Registrator](https://github.com/gliderlabs/registrator) to register your services you can pass the `urlprefix-` tags via the `SERVICE_TAGS` environment variable as follows: ``` $ docker run -d \ --name=registrator \ --net=host \ --volume=/var/run/docker.sock:/tmp/docker.sock \ gliderlabs/registrator:latest \ consul://localhost:8500 $ docker run -d -p 80:8000 \ -e SERVICE_8000_CHECK_HTTP=/foo/healthcheck \ -e SERVICE_8000_NAME=foo \ -e SERVICE_CHECK_INTERVAL=10s \ -e SERVICE_CHECK_TIMEOUT=5s \ -e SERVICE_TAGS=urlprefix-/foo \ test/foo ``` ### Docker Compose If you are using [Docker compose](https://docs.docker.com/compose/) you can add the `SERVICE_TAGS` to the `environment` section as follows: bar: environment: - SERVICE_TAGS=urlprefix-/bar ================================================ FILE: docs/content/feature/dynamic-reloading.md ================================================ --- title: "Dynamic Reloading" since: "1.0" --- fabio builds the routing table from the Consul service registrations, health check status and the user provided `route` commands stored in the Consul KV store. This is **the** core feature of fabio - the reason it exists. The cluster wide state is stored in the Consul Raft log which provides a consistent view of the available and healthy services in the cluster. When the Raft log changes fabio is notified and downloads the list of healthy services and the user defined routes from the KV store and re-builds the routing table. Once the new routing table has been built it is atomically swapped with the active routing table without any service interruption. Existing connections remain open and running requests are served even if the new routing table no longer contains that route. Registering or de-registering a service, setting a node to maintenance mode, failing or passing of a health check for a service, or writing data into the Consul KV store all trigger an automatic reload of the fabio routing table for all fabio nodes in the cluster. This all happens automatically, with no downtime, or manual intervention. ================================================ FILE: docs/content/feature/graceful-shutdown.md ================================================ --- title: "Graceful Shutdown" since: "1.0" --- fabio supports a graceful shutdown timeout during which new requests will receive a `503 Service Unavailable` response while the active requests can complete. See the `proxy.shutdownwait` option in the [fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties) file. ================================================ FILE: docs/content/feature/grpc-proxy.md ================================================ --- title: "GRPC Proxy" since: "1.5.11" --- fabio can run a transparent GRPC proxy which dynamically forwards an incoming RPC on a given port to services which advertise rpc service or method. To use GRPC proxy support the service needs to advertise `urlprefix-/my.service/Method proto=grpc` in Consul. In addition, fabio needs to be configured with a grpc listener: ``` fabio -proxy.addr ':1234;proto=grpc' ``` As per the HTTP/2 spec, the host header is not required, so host matching is not supported for GRPC proxying. To 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`. For example in Go grpc request: ``` md := metadata.Pairs("dsthost", "testonly") ctx := metadata.NewOutgoingContext(context.Background(), md) ``` To make RPC using the context with the metadata will try to access the service with tag ``` urlprefix-testonly/my.service/Method proto=grpc ``` first, if no such service exits, fall back to the service with tag ``` urlprefix-/my.service/Method proto=grpc ``` Equivalent metadata in Java: ``` public class GrpcClientHeaderInterceptor implements ClientInterceptor { private String dstHost; public GrpcClientHeaderInterceptor(String dstHost) { this.dstHost = dstHost; } @VisibleForTesting static final Metadata.Key CUSTOM_HEADER_KEY = Metadata.Key.of("dsthost", Metadata.ASCII_STRING_MARSHALLER); @Override public ClientCall interceptCall(MethodDescriptor method, CallOptions callOptions, Channel next) { ClientCall call = next.newCall(method, callOptions); return new SimpleForwardingClientCall(call) { @Override public void start(Listener responseListener, Metadata headers) { /* put custom header */ headers.put(CUSTOM_HEADER_KEY, "testonly"); } }; } } ``` GRPC proxy support can be combined with [Certificate Stores](/feature/certificate-stores/) to provide TLS termination on fabio. Configure `proxy.addr` with `proto=grpcs`. ``` fabio -proxy.cs 'cs=ssl;type=path;path=/etc/ssl' -proxy.addr ':1234;proto=grpcs;cs=ssl' ``` To support TLS upstream servers add the `proto=grpcs` option to the `urlprefix-` tag. The current implementation uses the clientca specified in the [Certificate Store](/feature/certificate-stores/) for the listener. To disable certificate validation for a target set the `tlsskipverify=true` option. ``` urlprefix-/foo proto=grpcs urlprefix-/foo proto=grpcs tlsskipverify=true ``` For 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=` in the `urlprefix-` tag. ``` urlprefix-/ proto=grpcs grpcservername=my.service.hostname ``` ================================================ FILE: docs/content/feature/http-compression.md ================================================ --- title: "HTTP Compression" since: "1.3.4" --- Enable dynamic compression of responses when the client sets the `Accept-Encoding: gzip` header and the name of the requested file matches a regular expression. To configure which files should be compressed on the fly set configure a regular expression in the `proxy.gzip.contenttype` property ``` # proxy.gzip.contenttype configures which responses should be compressed. # # By default, responses sent to the client are not compressed even if the # client accepts compressed responses by setting the 'Accept-Encoding: gzip' # header. By setting this value responses are compressed if the Content-Type # header of the response matches and the response is not already compressed. # The list of compressable content types is defined as a regular expression. # The regular expression must follow the rules outlined in golang.org/pkg/regexp. # # A typical example is # # proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\+(json|xml))(;.*)?$ # # The default is # # proxy.gzip.contenttype = ``` ================================================ FILE: docs/content/feature/http-headers.md ================================================ --- title: "HTTP Header Support" since: "1.1.3" --- In addition, to injecting the `Forwarded` and `X-Real-Ip` headers the `X-Forwarded-For`, `X-Forwarded-Port` and `X-Forwarded-Proto` headers are added to HTTP(S) and Websocket requests. Custom headers for the ip address and protocol can be configured with the `proxy.header.clientip`, `proxy.header.tls` and `proxy.header.tls.value` options. Since version 1.5.3 fabio also sets the `X-Forwarded-Host` header. ================================================ FILE: docs/content/feature/http-path-prepending.md ================================================ --- title: "HTTP Path Prepending" since: "1.5.14" --- fabio supports prepending a path to the incoming request. If you want to forward `http://host/bar` as `http://host/foo/bar` you can add a `prepend=/foo` option to the route options as `urlprefix-/bar prepend=/foo`. Path prepending is done after path stripping. If you want to forward `http://host/foo/bar` as `http://host/baz/bar` you can add `prepend=/baz` and `strip=/foo` options to the route options as `urlprefix-/bar prepend=/baz strip=/foo`. ================================================ FILE: docs/content/feature/http-path-stripping.md ================================================ --- title: "HTTP Path Stripping" since: "1.3.7" --- fabio supports stripping a path from the incoming request. If you want to forward `http://host/foo/bar` as `http://host/bar` you can add a `strip=/foo` option to the route options as `urlprefix-/foo/bar strip=/foo`. ================================================ FILE: docs/content/feature/http-redirects.md ================================================ --- title: "HTTP Redirects" since: "1.5.4" --- To redirect an HTTP request to another URL you can use the `redirect=` option. The `code` is the HTTP status code used for the redirect response and must be between 300-399 for the route to be valid. # redirect /path to https://www.google.com/ route add svc /path https://www.google.com/ opts "redirect=301" To use the redirect with the `urlprefix-` tags you need to specify the target URL in after the code since the target of the request is usually the address of the service that registers the tag. urlprefix-/path redirect=301,https://www.google.com/ If you want to include the original request URI in the redirect target append the `$path` pseudo-variable to the target URL. urlprefix-/path redirect=303,https://www.foo.com$path To redirect from HTTP to HTTPS you must include the `host:port` of the HTTP endpoint: route add svc example.com:80/ https://example.com/ opts "redirect=301" ================================================ FILE: docs/content/feature/https-tcp-sni-proxy.md ================================================ --- title: "HTTPS TCP-SNI Proxy" since: "1.5.14" --- fabio can run a TCP+SNI routing proxy on a listener, and have fallback to https functionality. This is effectively an amalgam of the TCP-SNI Proxy and the HTTPS functionality. To enable this feature configure a listener as follows: ``` fabio -proxy.addr=':443;proto=https+tcp+sni;cs=somecertstore' ``` For host matches that are proto=tcp or have a scheme of tcp://, this will proxy TCP using SNI. You would register your service in [Consul](https://consul.io) with a `urlprefix-` tag that matches the host from the SNI extension for any services that should be proxied TCP (TLS terminated by upstream). If the upstream service you'd like to proxy TCP responds to `https://foo.com/...` then you should register a `urlprefix-foo.com/ proto=tcp` tag for this service. For path based matching, you would do the typical `urlprefix-/path/` and this would cause fabio to terminate TLS using the cs= line specified in the config. ================================================ FILE: docs/content/feature/https-upstream.md ================================================ --- title: "HTTPS Upstream" since: "1.4.2" --- To support HTTPS upstream servers add the `proto=https` option to the `urlprefix-` tag. The current implementation requires that upstream certificates need to be in the system root CA list. To disable certificate validation for a target set the `tlsskipverify=true` option. ``` urlprefix-/foo proto=https urlprefix-/foo proto=https tlsskipverify=true ``` ================================================ FILE: docs/content/feature/metrics.md ================================================ --- title: "Metrics" since: "1.0.0 (Graphite), 1.2.1 (StatsD, DataDog, Circonus), 1.6.0 (Prometheus)" --- Fabio collects metrics per route and service instance as well as running totals to avoid computing large amounts of metrics. The metrics can be sent to [Circonus](http://www.circonus.com), [Graphite](https://graphiteapp.org), [StatsD](https://github.com/etsy/statsd), [DataDog](https://www.datadoghq.com) (via statsd - or since v1.6.0 to native protocol with tag support) or stdout. See the `metrics.*` options in the [fabio.properties](https://github.com/eBay/fabio/blob/master/fabio.properties) file. Prometheus is also possible, but it works the reverse of the other metrics platforms. Instead of pushing data to a metrics server, prometheus expects to poll an endpoint for changes. ### Configuring Prometheus Metrics To configure prometheus metrics, you need to do the following: 1) You must specify that prometheus is the [metrics.target](/ref/metrics.target/) 2) You must configure a listener in [proxy.addr](/ref/proxy.addr/) with `proto=prometheus` 3) (optional) override the [metrics.prometheus.path](/ref/metrics.prometheus.path/), [metrics.prometheus.subsystem](/ref/metrics.prometheus.subsystem/), and [metrics.prometheus.buckets](/ref/metrics.prometheus.buckets/). ### Metrics info (for non-tagged backends, such as circonus and statsd_raw) Fabio reports the following metrics: Name | Type | Description --------------------------- | -------- | ------------- `{route}.rx` | timer | Number of bytes received by fabio for TCP target `{route}.tx` | timer | Number of bytes transmitted by fabio for TCP target `{route}` | timer | Average response time for a route `http.status.code.{code}` | timer | Average response time for all HTTP(S) requests per status code `notfound` | counter | Number of failed HTTP route lookups `requests` | timer | Average response time for all HTTP(S) requests `grpc.requests` | timer | Average response time for all GRPC(S) requests `grpc.noroute` | counter | Number of failed GRPC route lookups `grpc.conn` | counter | Number of established GRPC proxy connections `grpc.status.{code}` | timer | Average response time for all GRPC(S) requests per status code `tcp.conn` | counter | Number of established TCP proxy connections `tcp.connfail` | counter | Number of TCP upstream connection failures `tcp.noroute` | counter | Number of failed TCP upstream route lookups `tcp_sni.conn` | counter | Number of established TCP+SNI proxy connections `tcp_sni.connfail` | counter | Number of failed TCP+SNI proxy connections `tcp_sni.noroute` | counter | Number of failed TCP+SNI upstream route lookups `ws.conn` | gauge | Number of actively open websocket connections ### Legend #### timer A timer counts events and provides an average throughput and latency number. Depending on the metrics provider the aggregation happens either in the metrics library (go-metrics: statsd, graphite) or in the system of the metrics provider (Circonus) #### counter A counter counts events and provides an monotonically increasing value. #### gauge A gauge provides a current value. #### {code} `{code}` is the three digit HTTP status code like `200`. #### {route} `{route}` is a shorthand for the metrics name generated for a route with the `metrics.names` template defined in [fabio.properties](https://github.com/fabiolb/fabio/blob/master/fabio.properties) ================================================ FILE: docs/content/feature/proxy-protocol.md ================================================ --- title: "PROXY Protocol Support" since: "1.1.3" --- fabio transparently supports the HA Proxy [PROXY protocol](http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) version 1 which is used by HA Proxy, [Amazon ELB](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/enable-proxy-protocol.html) and others to transmit the remote address and port of the client without using headers. You may control the behavior of PROXY protocol support with the following options on the listener: * `pxyproto`: When set to 'true' the listener will respect upstream v1 PROXY protocol headers. NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10. This changed to off when this option was introduced with the 1.5.11 release. For more information about the PROXY protocol, please see: http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt * `pxytimeout`: Sets PROXY protocol header read timeout as a duration (e.g. '250ms'). This defaults to 250ms if not set when 'pxyproto' is enabled. See the comments in for `proxy.addr` in `fabio.properties` for more information. ================================================ FILE: docs/content/feature/sse.md ================================================ --- title: "Server Sent Events (SSE)" since: "1.3" --- fabio detects [SSE](http://www.w3.org/TR/eventsource/) connections if the `Accept` header is set to `text/event-stream` and enables automatic flushing of the response buffer to forward data to the client. The default is set to `1s` and can be configured with the `proxy.flushinterval` parameter. ================================================ FILE: docs/content/feature/tcp-dynamic-proxy.md ================================================ --- title: "TCP Dynamic Proxy" --- The TCP dynamic proxy is similar to the TCP Proxy, but the listener is started from the Consul urlprefix tag. Also, the service is defined with IP and port, so that multiple services can be defined on the load balancer using the same TCP port. Connections are forwarded to services based on the combination of ip:port To use TCP Dynamic proxy support the service needs to advertise `urlprefix-127.0.0.1:1234 proto=tcp` in Consul. In addition, fabio needs to be configured with a placeholder for the proxy.addr.: ``` fabio -proxy.addr '0.0.0.0:0;proto=tcp-dynamic;refresh=5s' ``` The TCP listener is started for the given TCP ports. To use IP addressing to separate the services, matching IP addressed would need to be added to the loopback interface on the host. ================================================ FILE: docs/content/feature/tcp-proxy.md ================================================ --- title: "TCP Proxy" since: "1.4" --- fabio can run a transparent TCP proxy which dynamically forwards an incoming connection on a given port to services which advertise that port. To use TCP proxy support the service needs to advertise `urlprefix-:1234 proto=tcp` in Consul. In addition, fabio needs to be configured to listen on that port: ``` fabio -proxy.addr ':1234;proto=tcp' ``` TCP proxy support can be combined with [Certificate Stores](/feature/certificate-stores/) to provide TLS termination on fabio. ``` fabio -proxy.cs 'cs=ssl;type=path;cert=/etc/ssl' -proxy.addr ':1234;proto=tcp;cs=ssl' ``` ================================================ FILE: docs/content/feature/tcp-sni-proxy.md ================================================ --- title: "TCP-SNI Proxy" since: "1.3" --- fabio can run a transparent TCP proxy with SNI support which can forward any TLS connection **without re-encrypting the traffic**. fabio captures the `ClientHello` packet which is the first packet of the TLS handshake and extracts the server name from the SNI extension and uses it for finding the upstream server to forward the connection to. It then replays the `ClientHello` packet and then transparently forwards all traffic between client and server as a byte stream. To enable this feature configure a listener as follows: ``` fabio -proxy.addr=':443;proto=tcp+sni' ``` to listen to more than 1 port separate with comma's (like if you want to do tcp and http listening): ``` fabio -proxy.addr ':9999,:19587;proto=tcp ``` This will do normal fabio http(s) routing on port 9999 and TCP proxy on port 19587. and register your services in [Consul](https://consul.io/) with a `urlprefix-` tag that matches the host from the SNI extension. If your server responds to `https://foo.com/...` then you should register a `urlprefix-foo.com/` tag for this service. Note that the tag should only contain `/` since path-based routing is not possible with this approach. ================================================ FILE: docs/content/feature/traffic-shaping.md ================================================ --- title: "Traffic Shaping" since: "1.0" --- fabio allows to control the amount of traffic a set of service instances will receive. You can use this feature to direct a fixed percentage of traffic to a newer version of an existing service for testing ("Canary testing"). See [Config Language](../../cfg) for a complete description of the `route weight` command. The following command will allocate 5% of traffic to `www.kjca.dev/auth/` to all instances of `service-b` which match tags `version-15` and `dc-fra`. This is independent of the number of actual instances running. The remaining 95% of the traffic will be distributed evenly across the remaining instances publishing the same prefix. ``` route weight service-b www.kjca.dev/auth/ weight 0.05 tags "version-15,dc-fra" ``` ### Vault Example [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. The following command will allocate 100% of traffic to `vault.company.com` to the instance of `vault` which is registered with the tag `active`. ``` route weight vault vault.company.com weight 1.00 tags "active" ``` ================================================ FILE: docs/content/feature/vault.md ================================================ --- title: "Vault Support" since: "1.2 (Vault KV), 1.5.3 (Vault PKI)" --- fabio can use [Vault](https://vaultproject.io) as a secure key/value store to store certificates. As of 1.5.3 fabio can use the PKI support of Vault to generate TLS certificates on demand. See [fabio.properties](https://github.com/fabiolb/fabio/blob/master/fabio.properties) for details. ================================================ FILE: docs/content/feature/web-ui.md ================================================ --- title: "Web UI" sincd: "1.0" --- fabio supports a Web UI to examine the current routing table and manage the manual overrides. By default it listens on `http://0.0.0.0:9998/` which can be changed with the `ui.addr` option. The `ui.title` and `ui.color` options allow customization of the title and the color of the header bar. ================================================ FILE: docs/content/feature/websockets.md ================================================ --- title: "Websockets" since: "1.0.5" --- fabio transparently supports Websocket connections by detecting the `Upgrade: websocket` header in the incoming HTTP(S) request. Websocket support has been implemented with the websocket library from [golang.org/x/net/websocket](http://golang.org/x/net/websocket). You can test the websocket support with the `demo/wsclient` and `demo/server` which implements a simple echo server. ./server -addr 127.0.0.1:5000 -name ws-a -prefix /echo -proto ws ./wsclient -url ws://127.0.0.1:9999/echo You can also run multiple web socket servers on different ports but the same endpoint. fabio detects on whether to forward the request as HTTP or WS based on the value of the `Upgrade` header. If the value is `websocket` it will attempt a websocket connection to the target. Otherwise, it will fall back to HTTP. One limitation of the current implementation is that the accepted set of protocols has to be symmetric across all services handling it. Only the following combinations will work reliably: svc-a and svc-b register /foo and accept only HTTP traffic there svc-a and svc-b register /foo and accept only WS traffic there svc-a and svc-b register /foo and accept both HTTP and WS traffic there The following setup (or variations thereof) will not work reliably: svc-a registers /foo and accept only WS traffic there svc-b registers /foo and accept only HTTP traffic there This is not a limitation of the routing itself but because the current configuration does not provide fabio with enough information to make the routing decision since the services do not advertise the protocols they handle on a given endpoint. This does not look like a big restriction but is also not difficult to extend in a later version assuming there are use cases which require this behavior. For now the services have to be symmetric in the protocols they accept. ================================================ FILE: docs/content/quickstart/_index.md ================================================ --- title: "Quickstart" weight: 100 --- 1. Install from source, binary, Docker or Homebrew. ``` go get github.com/fabiolb/fabio (>= go1.15) brew install fabio (OSX/macOS stable) docker pull fabiolb/fabio (Docker) https://github.com/fabiolb/fabio/releases (pre-built binaries) ``` 2. Register your service in Consul. Make sure that each instance registers with a unique `ServiceID` and a service name **without spaces**. 3. Register a health check in Consul as described [here](https://www.consul.io/docs/agent/checks.html). Make sure the health check is since fabio will only watch services which have a passing health check. 4. Routes are stored in Consul [Service Tags](https://www.consul.io/docs/agent/services.html) and you need to add a separate `urlprefix-` tag for every `host/path` prefix the service serves. For example, if your service handles `/user` and `/product` then add two tags `urlprefix-/user` and `urlprefix-/product`. You can register as many prefixes as you want. fabio can forward HTTP, HTTPS and TCP traffic. Below are some configuration examples: ``` # HTTP/S examples # Make sure the prefix for HTTP routes contains at least one slash (/). urlprefix-/css # path route urlprefix-i.com/static # host specific path route urlprefix-mysite.com/ # host specific catch all route urlprefix-/foo/bar strip=/foo # path stripping (forward '/bar' to upstream) urlprefix-/bar prepend=/foo # path prepending (forward '/foo/bar' to upstream) urlprefix-/foo/bar proto=https # HTTPS upstream urlprefix-/foo/bar proto=https tlsskipverify=true # HTTPS upstream and self-signed cert # TCP examples urlprefix-:3306 proto=tcp # route external port 3306 urlprefix-:3306 proto=tcp pxyproto=true # enables PROXY protocol on outbount TCP connection # GRPC/S examples urlprefix-/my.service/Method proto=grpc # method specific route urlprefix-/my.service proto=grpc # service specific route urlprefix-/my.service proto=grpcs # TLS upstream urlprefix-/my.service proto=grpcs grpcservername=my.service # TLS upstream with servername override urlprefix-/my.service proto=grpcs tlsskipverify=true # TLS upstream and self-signed cert ``` 5. Start fabio without a config file ``` $ fabio ``` This assumes that a Consul agent is running on `localhost:8500`. Watch the log output how fabio picks up the route to your service. **Note:** For running fabio in Docker [look here](/feature/docker/). 6. Try starting/stopping your service to see how the routing table changes instantly. 7. Test that you can access the upstream service via fabio ``` # for urlprefix-/foo curl -i http://localhost:9999/foo # for urlprefix-mysite.com/foo curl -i -H 'Host: mysite.com' http://localhost:9999/foo ``` 8. Send all your HTTP traffic to fabio on port `9999` ================================================ FILE: docs/content/ref/_index.md ================================================ --- title: "Reference" weight: 600 --- All configuration options can be specified either * in the config file * as environment variable * as command line argument and are evaluated in that order. ``` # fabio.properties metrics.target = stdout # correspondig env var (no prefix) metrics_target=stdout ./fabio # env var with FABIO_ prefix (>= 1.2) FABIO_metrics_target=stdout ./fabio # env var with FABIO_ prefix (case-insensitive) (>= 1.2) FABIO_METRICS_TARGET=stdout ./fabio # command line argument (>= 1.2) ./fabio -metrics.target stdout ``` ================================================ FILE: docs/content/ref/bgp.anycastaddresses.md ================================================ --- title: "bgp.anycastaddresses" --- `bgp.anycastaddresses` sets the anycast addresses we will advertise, separated by comma. Technically this will advertise any route prefix. These should already be configured on the host probably hung off loopback. For example, 192.168.5.3/32. The default value is bgp.anycastaddresses = If bgp is enabled, this must be defined. ================================================ FILE: docs/content/ref/bgp.asn.md ================================================ --- title: "bgp.asn" --- `bgp.asn` sets the asn ID of our router The default value is bgp.asn = 65000 ================================================ FILE: docs/content/ref/bgp.certfile.md ================================================ --- title: "bgp.certfile" --- `bgp.certfile` is the file path of the certificate, and is required if bgp.grpctls is set to true. The default value is bgp.certfile = ================================================ FILE: docs/content/ref/bgp.enabled.md ================================================ --- title: "bgp.enabled" --- `bgp.enabled` enables the embedded gobgpd daemon The default value is bgp.enabled = false ================================================ FILE: docs/content/ref/bgp.enablegrpc.md ================================================ --- title: "bgp.enablegrpc" --- `bgp.enablegrpc` enables the gobgp grpc interface. To be used with the gobgp command line client. The default value is bgp.enablegrpc = false ================================================ FILE: docs/content/ref/bgp.gobgpdcfgfile.md ================================================ --- title: "bgp.gobgpdcfgfile" --- `bgp.gobgpdcfgfile` is the optional file path to a gobgpd [config file](https://github.com/osrg/gobgp/blob/master/docs/sources/configuration.md). This overrides the global config items, such as bgp.routerid, bgp.asn etc. This also skips automatically adding gobgpd [policies](https://github.com/osrg/gobgp/blob/master/docs/sources/policy.md) that restrict / disallow accepting prefixes from neighbors. Only use this if you know what you're doing, this is to allow for more flexibility than we expose directly with fabio. The default value is bgp.gobgpdcfgfile = ================================================ FILE: docs/content/ref/bgp.grpclistenaddress.md ================================================ --- title: "bgp.grpclistenaddress" --- `bgp.grpclistenaddress` is the listen interface and port if bgp.enablegrpc is set to true. The default value is bgp.grpclistenaddress = 127.0.0.1:50051 ================================================ FILE: docs/content/ref/bgp.grpctls.md ================================================ --- title: "bgp.grpctls" --- `bgp.grpctls` is whether to enable TLS on the bgp grpc interface. The default value is bgp.grpctls = false ================================================ FILE: docs/content/ref/bgp.keyfile.md ================================================ --- title: "bgp.keyfile" --- `bgp.keyfile` is the file path of the key file, and is required if bgp.grpctls is set to true. The default value is bgp.keyfile = ================================================ FILE: docs/content/ref/bgp.listenaddresses.md ================================================ --- title: "bgp.listenaddresses" --- `bgp.listenaddresses` sets the listen addresses for bgp, separated by comma. The default value is bgp.listenaddresses = 0.0.0.0 which listens on all interfaces. ================================================ FILE: docs/content/ref/bgp.listenport.md ================================================ --- title: "bgp.listenport" --- `bgp.listenport` sets the listen ports for bgp communication from other routers. The default value is bgp.listenport = 179 The default bgp port is 179. ================================================ FILE: docs/content/ref/bgp.nexthop.md ================================================ --- title: "bgp.nexthop" --- `bgp.nexthop` sets the next hop address. If not set, it uses the [bgp.routerid](/ref/bgp.routerid/) instead. The default value is bgp.nexthop = ================================================ FILE: docs/content/ref/bgp.peers.md ================================================ --- title: "bgp.peers" --- `bgp.peers` sets the bgp peers we will advertise routes to. This is required if bgp is enabled. bgp.peers is specified as a comma separated list of neighboraddress and asn pairs, i.e. bgp.peers = address=1.2.3.4;asn=65001,address=5.6.7.8;asn=65002 valid parameters for peers are: address - required port - optional, defaults to 179 asn - required multihop - optional, defaults to false multihoplength - optional, defaults to 2 password - optional The default value is bgp.peers = ================================================ FILE: docs/content/ref/bgp.routerid.md ================================================ --- title: "bgp.routerid" --- `bgp.routerid` is the router id (ip address) of this router. This is required if bgp is enabled. This should be the unique IP address, not any anycast. This will also be used as the default nexthop address unless [bgp.nexthop](/ref/bgp.nexthop/) is specified. The default value is bgp.routerid = ================================================ FILE: docs/content/ref/glob.cache.size.md ================================================ --- title: "glob.cache.size" --- `glob.cache.size` Sets the globCache size used for matching on route lookups. The default is glob.cache.size = 1000 ================================================ FILE: docs/content/ref/glob.matching.disabled.md ================================================ --- title: "glob.matching.disabled" --- `glob.matching.disabled` disables glob matching on route lookups. Valid options are `true`, `false` The default is glob.matching.disabled = false ================================================ FILE: docs/content/ref/log.access.format.md ================================================ --- title: "log.access.format" --- `log.access.format` configures the format of the access log. If the value is either `common` or `combined` then the logs are written in the Common Log Format or the Combined Log Format as defined below: * `common`: `$remote_host - - [$time_common] "$request" $response_status $response_body_size` * `combined`: `$remote_host - - [$time_common] "$request" $response_status $response_body_size "$header.Referer" "$header.User-Agent"` Otherwise, the value is interpreted as a custom log format which is defined with the following parameters. Providing an empty format when logging is enabled is an error. To disable access logging leave the `log.access.target` value empty. $header. - request http header (name: [a-zA-Z0-9-]+) $remote_addr - host:port of remote client $remote_host - host of remote client $remote_port - port of remote client $request - request $request_args - request query parameters $request_host - request host header (aka server name) $request_method - request method $request_scheme - request scheme $request_uri - request URI $request_url - request URL $request_proto - request protocol $response_body_size - response body size in bytes $response_status - response status code $response_time_ms - response time in S.sss format $response_time_us - response time in S.ssssss format $response_time_ns - response time in S.sssssssss format $time_rfc3339 - log timestamp in YYYY-MM-DDTHH:MM:SSZ format $time_rfc3339_ms - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format $time_rfc3339_us - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format $time_rfc3339_ns - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format $time_unix_ms - log timestamp in unix epoch ms $time_unix_us - log timestamp in unix epoch us $time_unix_ns - log timestamp in unix epoch ns $time_common - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ $upstream_addr - host:port of upstream server $upstream_host - host of upstream server $upstream_port - port of upstream server $upstream_request_scheme - upstream request scheme $upstream_request_uri - upstream request URI $upstream_request_url - upstream request URL $upstream_service - name of the upstream service The default is log.access.format = common ================================================ FILE: docs/content/ref/log.access.target.md ================================================ --- title: "log.access.target" --- `log.access.target` configures where the access log is written to. Options are `stdout`. If the value is empty no access log is written. The default is log.access.target = ================================================ FILE: docs/content/ref/log.level.md ================================================ --- title: "log.level" --- `log.level` configures the log level. Valid levels are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` and `FATAL`. The default is log.level = INFO ================================================ FILE: docs/content/ref/log.routes.format.md ================================================ --- title: "log.routes.format" --- `log.routes.format` configures the log output format of routing table updates. Changes to the routing table are written to the standard log. This option configures the output format: * `detail`: detailed routing table as ascii tree * `delta`: additions and deletions in config language * `all`: complete routing table in config language The default is log.routes.format = delta ================================================ FILE: docs/content/ref/metrics.circonus.apiapp.md ================================================ --- title: "metrics.circonus.apiapp" --- `metrics.circonus.apiapp` configures the API token app to use when submitting metrics to Circonus. See: https://login.circonus.com/user/tokens This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`. The default is metrics.circonus.apiapp = fabio ================================================ FILE: docs/content/ref/metrics.circonus.apikey.md ================================================ --- title: "metrics.circonus.apikey" --- `metrics.circonus.apikey` configures the API token key to use when submitting metrics to Circonus. See: https://login.circonus.com/user/tokens This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus` but [metrics.circonus.submissionurl](/ref/metrics.circonus.submissionurl/) is specified} The default is metrics.circonus.apikey = ================================================ FILE: docs/content/ref/metrics.circonus.apiurl.md ================================================ --- title: "metrics.circonus.apiurl" --- `metrics.circonus.apiurl` configures the API URL to use when submitting metrics to Circonus. https://api.circonus.com/v2/ will be used if no specific URL is provided. This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`. The default is metrics.circonus.apiurl = ================================================ FILE: docs/content/ref/metrics.circonus.brokerid.md ================================================ --- title: "metrics.circonus.brokerid" --- `metrics.circonus.brokerid` configures a specific broker to use when creating a check for submitting metrics to Circonus. This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`. Optional for public brokers, required for Inside brokers. Only applicable if a check is being created. The default is metrics.circonus.brokerid = ================================================ FILE: docs/content/ref/metrics.circonus.checkid.md ================================================ --- title: "metrics.circonus.checkid" --- `metrics.circonus.checkid` configures a specific check to use when submitting metrics to Circonus. This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus`. An attempt will be made to search for a previously created check, if no applicable check is found, one will be created. The default is metrics.circonus.checkid = ================================================ FILE: docs/content/ref/metrics.circonus.submissionurl.md ================================================ --- title: "metrics.circonus.submissionurl" --- `metrics.circonus.submissionurl` configures a specific check submission url for a Check API object of a previously created HTTPTRAP check. This is optional when [metrics.target](/ref/metrics.target/) is set to `circonus` but [metrics.circonus.apikey](/ref/metrics.circonus.apikey/) is specified}. #### Example `http://127.0.0.1:2609/write/fabio` The default is metrics.circonus.submissionurl = ================================================ FILE: docs/content/ref/metrics.dogstatsd.addr.md ================================================ --- title: "metrics.dogstatsd.addr" --- `metrics.dogstatsd.addr` configures the host:port of the dogstatsd server. This is required when [metrics.target](/ref/metrics.target/) is set to `dogstatsd`. The default is metrics.dogstatsd.addr = ================================================ FILE: docs/content/ref/metrics.graphite.addr.md ================================================ --- title: "metrics.graphite.addr" --- `metrics.graphite.addr` configures the `host:port` of the Graphite server. This is required when [metrics.target](/ref/metrics.target/) is set to `graphite`. The default is metrics.graphite.addr = ================================================ FILE: docs/content/ref/metrics.interval.md ================================================ --- title: "metrics.interval" --- `metrics.interval` configures the interval in which metrics are reported. The default is metrics.interval = 30s ================================================ FILE: docs/content/ref/metrics.names.md ================================================ --- title: "metrics.names" --- `metrics.names` configures the template for the route metric names on backends that don't support tags. This is used in circonus, graphite and statsd_raw. dogstatsd and prometheus ignore this. The value is expanded by the [text/template](https://golang.org/pkg/text/template) package and provides the following variables: * `Service`: the service name * `Host`: the host part of the URL prefix * `Path`: the path part of the URL prefix * `TargetURL`: the URL of the target The following additional functions are defined: * `clean`: lowercase value and replace `.` and `:` with `_` Given a route rule of route add testservice www.example.com/ http://10.1.2.3:12345/ the template variables are: .Service = testservice .Host = www.example.com .Path = / .TargetURL.Host = 10.1.2.3:12345 which results to the following metric name when using the default template: testservice.www_example_com./.10_1_2_3_12345 The default is metrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}} ================================================ FILE: docs/content/ref/metrics.prefix.md ================================================ --- title: "metrics.prefix" --- `metrics.prefix` configures the template for the prefix of all reported metrics. Each metric has a unique name which is hard-coded to prefix.service.host.path.target-addr The value is expanded by the text/template package and provides the following variables: * `Hostname`: the Hostname of the server * `Exec`: the executable name of application The following additional functions are defined: * `clean`: lowercase value and replace `.` and `:` with `_` Template may include regular string parts to customize final prefix #### Example Server hostname: `test-001.something.com` Binary executable name: `fabio` The template variables are: .Hostname = test-001.something.com .Exec = fabio which results to the following prefix string when using the default template: test-001_something_com.fabio The default is metrics.prefix = {{clean .Hostname}}.{{clean .Exec}} ================================================ FILE: docs/content/ref/metrics.prometheus.buckets.md ================================================ --- title: "metrics.prometheus.buckets" --- `metrics.prometheus.buckets` configures the time buckets for use with histograms, measured in seconds. for instance, .005 is equivalent to 5ms. There is an implied "infinity" bucket tacked on at the end. The default is `metrics.prometheus.buckets = .005,.01,.025,.05,.1,.25,.5,1,2.5,5,10` ================================================ FILE: docs/content/ref/metrics.prometheus.path.md ================================================ --- title: "metrics.prometheus.path" --- `metrics.prometheus.path` configures the path to serve up metrics on any configured [proxy.addr](/ref/proxy.addr/) where `proto=prometheus`. Defaults to `/metrics` ================================================ FILE: docs/content/ref/metrics.prometheus.subsystem.md ================================================ --- title: "metrics.prometheus.subsystem" --- `metrics.prometheus.subsystem` configures the subsystem name when reporting metrics. This is basically appended to the prefix for metric names. See https://prometheus.io/docs/practices/instrumentation/#subsystems for more information. ================================================ FILE: docs/content/ref/metrics.retry.md ================================================ --- title: "metrics.retry" --- `metrics.retry` configures the interval with which fabio tries to connect to the metrics backend during startup. The default is metrics.retry = 500ms ================================================ FILE: docs/content/ref/metrics.statsd.addr.md ================================================ --- title: "metrics.statsd.addr" --- `metrics.statsd.addr` configures the host:port of the StatsD server. This is required when [metrics.target](/ref/metrics.target/) is set to `statsd_raw`. The default is metrics.statsd.addr = ================================================ FILE: docs/content/ref/metrics.target.md ================================================ --- title: "metrics.target" --- `metrics.target` configures the backend the metrics values are sent to. Possible values are: * ``: do not report metrics * `stdout`: report metrics to stdout * `graphite`: report metrics to Graphite on [metrics.graphite.addr](/ref/metrics.graphite.addr/) * `statsd`: legacy statsd support, used in v1.5.5 and lower - removed in v1.6 * `statsd_raw`: report metrics to StatsD on [metrics.statsd.addr](/ref/metrics.statsd.addr/) - this was intentionally renamed because anyone upgrading to 1.6 will need to revisit their configuration anyway due to rewrite of this backend. It was quite broken before, the counters never reset, it did not follow the spec so the info was likely wrong or people using this were doing some workarounds they'll need to remove anyway. * `circonus`: report metrics to Circonus (https://circonus.com/) * `prometheus`: use prometheus metrics. (https://prometheus.io) Must be used in conjuction with a prometheus listener in [proxy.addr](/ref/proxy.addr/) * `dogstatsd`: use with datadog dogstatsd (https://www.datadoghq.com/) The default is metrics.target = Multiple metrics targets can be defined separated by comma. ================================================ FILE: docs/content/ref/metrics.timeout.md ================================================ --- title: "metrics.timeout" --- `metrics.timeout` configures how long fabio tries to connect to the metrics backend during startup. The default is metrics.timeout = 10s ================================================ FILE: docs/content/ref/proxy.addr.md ================================================ --- title: "proxy.addr" --- `proxy.addr` configures listeners. Each listener is configured with and address and a list of optional arguments in the form of [host]:port;opt=arg;opt[=arg];... Each listener has a protocol which is configured with the `proto` option for which it routes and forwards traffic. The supported protocols are: * `http` for HTTP based protocols * `https` for HTTPS based protocols * `grpc` for GRPC based protocols * `grpcs` for GRPC+TLS based protocols * `tcp` for a raw TCP proxy with or witout TLS support * `tcp+sni` for an SNI aware TCP proxy * `tcp-dynamic` for a consul driven TCP proxy * `https+tcp+sni` for an SNI aware TCP proxy with https fallthrough * `prometheus` for a prometheus metrics endpoint. Used in conjunction with [metrics.target](/ref/metrics.target/) =prometheus If no `proto` option is specified then the protocol is either `http` or `https` depending on whether a certificate source is configured via the `cs` option which contains the name of the certificate source. The TCP+SNI proxy analyzes the `ClientHello` message of TLS connections to extract the server name extension and then forwards the encrypted traffic to the destination without decrypting the traffic. #### General options * `rt`: Sets the read timeout as a duration value (e.g. `3s`) * `wt`: Sets the write timeout as a duration value (e.g. `3s`) * `it`: Sets the idle timeout as a duration value (e.g. `3s`) * `strictmatch`: When set to `true` the certificate source must provide a certificate that matches the hostname for the connection to be established. Otherwise, the first certificate is used if no matching certificate was found. This matches the default behavior of the Go TLS server implementation. * `pxyproto`: When set to 'true' the listener will respect upstream v1 PROXY protocol headers. NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10. This changed to off when this option was introduced with the 1.5.11 release. For more information about the PROXY protocol, please see: http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt * `pxytimeout`: Sets PROXY protocol header read timeout as a duration (e.g. '250ms'). This defaults to 250ms if not set when `pxyproto` is enabled. * `refresh`: Sets the refresh interval to check the route table for updates. Used when `tcp-dynamic` is enabled. #### TLS options * `tlsmin`: Sets the minimum TLS version for the handshake. This value is one of `ssl30`, `tls10`, `tls11`, `tls12` or the corresponding version number from https://golang.org/pkg/crypto/tls/#pkg-constants * `tlsmax`: Sets the maximum TLS version for the handshake. See `tlsmin` for the format. * `tlsciphers`: Sets the list of allowed ciphers for the handshake. The value is a quoted comma-separated list of the hex cipher values or the constant names from https://golang.org/pkg/crypto/tls/#pkg-constants, e.g. `"0xc00a,0xc02b"` or `"TLS_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_128_CBC_SHA"` #### Examples # HTTP listener on port 9999 proxy.addr = :9999 # HTTP listener on IPv4 with read timeout proxy.addr = 1.2.3.4:9999;rt=3s # HTTP listener on IPv6 with write timeout proxy.addr = [2001:DB8::A/32]:9999;wt=5s # Multiple listeners proxy.addr = 1.2.3.4:9999;rt=3s,[2001:DB8::A/32]:9999;wt=5s # Multiple listeners with different protocols and options proxy.addr = 172.16.20.11:80;proto=http;rt=60s;wt=30s, \ 172.16.20.11:443;proto=https;rt=60s;wt=30s;cs=all;tlsmin=10, \ 172.16.20.11:8443;proto=tcp+sni # HTTPS listener on port 443 with certificate source proxy.addr = :443;cs=some-name # HTTPS listener on port 443 with certificate source and TLS options proxy.addr = :443;cs=some-name;tlsmin=tls10;tlsmax=tls11;tlsciphers="0xc00a,0xc02b" # GRPC listener on port 8888 proxy.addr = :8888;proto=grpc # GRPCS listener on port 8888 with certificate source proxy.addr = :8888;proto=grpcs;cs=some-name # TCP listener on port 1234 with port routing proxy.addr = :1234;proto=tcp # TCP listener on port 443 with SNI routing proxy.addr = :443;proto=tcp+sni # TCP listener on port 443 with SNI routing with HTTPS fallthrough proxy.addr = :443;proto=https+tcp+sni;cs=some-name # TCP listeners using consul for config with 5 second refresh interval proxy.addr = 0.0.0.0:0;proto=tcp-dynamic;refresh=5s The default is proxy.addr = :9999 ================================================ FILE: docs/content/ref/proxy.auth.md ================================================ --- title: "proxy.auth" --- `proxy.auth` configures one or more authorization schemes. Each authorization scheme is configured with a list of key/value options. Each scheme must have a unique name which can then be referred to in a routing rule. name=;type=;opt=arg;opt[=arg];... The following types of authorization schemes are available: #### Basic The 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. The `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. Note: removing the htpasswd file will cause all requests to fail with HTTP status code 401 (Unauthorized) until the file is restored. name=;type=basic;file=;realm=;refresh= Supported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd) #### Examples # single basic auth scheme name=mybasicauth;type=basic;file=p/creds.file; # single basic auth scheme with refresh interval set to 30 seconds name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s # basic auth with multiple schemes proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s, name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm The default is proxy.auth = ================================================ FILE: docs/content/ref/proxy.cs.md ================================================ --- title: "proxy.cs" --- `proxy.cs` configures one or more certificate sources. Each certificate source is configured with a list of key/value options. Each source must have a unique name which can then be referred to in a listener configuration. cs=;type=;opt=arg;opt[=arg];... All certificates need to be provided in PEM format. The following types of certificate sources are available: #### File The `file` certificate source supports one certificate which is loaded at startup and is cached until the service exits. The `cert` option contains the path to the certificate file. The `key` option contains the path to the private key file. If the certificate file contains both the certificate and the private key the `key` option can be omitted. The `clientca` option contains the path to one or more client authentication certificates. cs=;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem #### Path The `path` certificate source loads certificates from a directory in alphabetical order and refreshes them periodically. The `cert` option provides the path to the TLS certificates and the `clientca` option provides the path to the certificates for client authentication. TLS certificates are stored either in one or two files: www.example.com.pem or www.example.com-{cert,key}.pem TLS certificates are loaded in alphabetical order and the first certificate is the default for clients which do not support SNI. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. The default refresh interval is 3 seconds and cannot be lower than 1 second to prevent busy loops. To load the certificates only once and disable automatic refreshing set `refresh` to zero. cs=;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s #### HTTP The `http` certificate source loads certificates from an HTTP/HTTPS server. The `cert` option provides a URL to a text file which contains all files that should be loaded from this directory. The filenames follow the same rules as for the path source. The text file can be generated with: ls -1 *.pem > list The `clientca` option provides a URL for the client authentication certificates analogous to the `cert` option. Authentication credentials can be provided in the URL as request parameter, as basic authentication parameters or through a header. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. The default refresh interval is 3 seconds and cannot be lower than 1 second to prevent busy loops. To load the certificates only once and disable automatic refreshing set `refresh` to zero. cs=;type=http;cert=https://host.com/path/to/cert/list&token=123 cs=;type=http;cert=https://user:pass@host.com/path/to/cert/list cs=;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234 #### Consul The `consul` certificate source loads certificates from Consul. The `cert` option provides a KV store URL where the the TLS certificates are stored. The `clientca` option provides a URL to a path in the KV store where the the client authentication certificates are stored. The filenames follow the same rules as for the path source. The TLS certificates are updated automatically whenever the KV store changes. The client authentication certificates cannot be updated automatically since Go does not provide a mechanism for that yet. cs=;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123 #### Vault The `vault` certificate store uses HashiCorp Vault as the certificate store. The `cert` option provides the path to the TLS certificates and the `clientca` option provides the path to the certificates for client authentication. The `refresh` option can be set to specify the refresh interval for the TLS certificates. Client authentication certificates cannot be refreshed since Go does not provide a mechanism for that yet. The default refresh interval is 3 seconds and cannot be lower than 1 second to prevent busy loops. To load the certificates only once and disable automatic refreshing set `refresh` to zero. The path to vault must be provided in the VAULT_ADDR environment variable. The token must be provided in the VAULT_TOKEN environment variable. cs=;type=vault;cert=secret/fabio/certs #### Vault PKI The `vault-pki` certificate store uses HashiCorp Vault's PKI backend to issue certificates on-demand. The `cert` option provides a PKI backend path for issuing certificates. The `clientca` option works in the same way as for the generic Vault source. The `refresh` option determines how long before the expiration date certificates are re-issued. Values smaller than one hour are silently changed to one hour, which is also the default. cs=;type=vault-pki;cert=pki/issue/example-dot-com;refresh=24h;clientca=secret/fabio/client-certs This source will issue server certificates on-demand using the PKI backend and re-issue them 24 hours before they expire. The CA for client authentication is expected to be stored at secret/fabio/client-certs. #### Common options All certificate stores support the following options: caupgcn: Upgrade a self-signed client auth certificate with this common-name to a CA certificate. Typically used for self-singed certificates for the Amazon AWS Api Gateway certificates which do not have the CA flag set which makes them unsuitable for client certificate authentication in Go. For the AWS Api Gateway set this value to `ApiGateway` to allow client certificate authentication. This replaces the deprecated parameter `aws.apigw.cert.cn` which was introduced in version 1.1.5. #### Examples # file based certificate source proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem # path based certificate source proxy.cs = cs=some-name;type=path;path=path/to/certs # HTTP certificate source proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs # Consul certificate source proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123 # Vault certificate source proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs # Vault PKI certificate source proxy.cs = cs=some-name;type=vault-pki;cert=pki/issue/example-dot-com # Multiple certificate sources proxy.cs = cs=srcA;type=path;path=path/to/certs,\ cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs # path based certificate source for AWS Api Gateway proxy.cs = cs=some-name;type=path;path=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway The default is proxy.cs = ================================================ FILE: docs/content/ref/proxy.deregistergraceperiod.md ================================================ --- title: "proxy.deregistergraceperiod" --- `proxy.deregistergraceperiod` configures the time to wait before shutting down the proxies de-registering from the service registry. After a signal is caught Fabio will immediately de-register from the service registry and wait for `proxy.deregistergraceperiod` letting in-flight requests finish after which it will continue with shutting down the proxy. The default is proxy.deregistergraceperiod = 0s ================================================ FILE: docs/content/ref/proxy.dialtimeout.md ================================================ --- title: "proxy.dialtimeout" --- `proxy.dialtimeout` configures the connection timeout for outgoing connections by setting the [Timeout](https://golang.org/pkg/net/#Dialer.Timeout) of the [net.Dialer](https://golang.org/pkg/net/#Dialer) The default is proxy.dialtimeout = 30s ================================================ FILE: docs/content/ref/proxy.flushinterval.md ================================================ --- title: "proxy.flushinterval" --- `proxy.flushinterval` configures periodic flushing of the response buffer for SSE (server-sent events) connections. They are detected when the `Accept` header is `text/event-stream`. The default is proxy.flushinterval = 1s ================================================ FILE: docs/content/ref/proxy.globalflushinterval.md ================================================ --- title: "proxy.globalflushinterval" --- `proxy.globalflushinterval` configures periodic flushing of the response buffer for proxied non-SSE connections. By default it is disabled. The default is proxy.globalflushinterval = 0 ================================================ FILE: docs/content/ref/proxy.grpcmaxrxmsgsize.md ================================================ --- title: "proxy.grpcmaxrxmsgsize" --- `proxy.grpcmaxrxmsgsize` configures the grpc max receive message size in bytes. The default value is proxy.grpcmaxrxmsgsize = 4194304 which is 4MB ================================================ FILE: docs/content/ref/proxy.grpcmaxtxmsgsize.md ================================================ --- title: "proxy.grpcmaxtxmsgsize" --- `proxy.grpcmaxtxmsgsize` configures the grpc max transmit message size in bytes. The default value is proxy.grpcmaxtxmsgsize = 4194304 which is 4MB ================================================ FILE: docs/content/ref/proxy.grpcshutdowntimeout.md ================================================ --- title: "proxy.grpcshutdowntimeout" --- `proxy.grpcshutdowntimeout` configures the amount of time fabio will wait to attempt to close the connection while waiting for grpc traffic to finish to a backend that's been deregistered. The default value is proxy.grpcshutdowntimeout = 2s ================================================ FILE: docs/content/ref/proxy.gzip.contenttype.md ================================================ --- title: "proxy.gzip.contenttype" --- `proxy.gzip.contenttype` configures which responses should be compressed. By default, responses sent to the client are not compressed even if the client accepts compressed responses by setting the 'Accept-Encoding: gzip' header. By setting this value responses are compressed if the `Content-Type` header of the response matches and the response is not already compressed. The list of compressable content types is defined as a regular expression. The regular expression must follow the rules outlined in https://golang.org/pkg/regexp. A typical example is proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\+(json|xml))(;.*)?$ The default is proxy.gzip.contenttype = ================================================ FILE: docs/content/ref/proxy.header.clientip.md ================================================ --- title: "proxy.header.clientip" --- `proxy.header.clientip` configures the header for the request ip. The remote ip address is taken from [http.Request.RemoteAddr](https://golang.org/pkg/net/http/#Request.RemoteAddr). The default is proxy.header.clientip = ================================================ FILE: docs/content/ref/proxy.header.requestid.md ================================================ --- title: "proxy.header.requestid" --- `proxy.header.requestid` configures the header for the adding a unique request id. When set non-empty value the proxy will set this header on every request to the unique UUID value. The default is proxy.header.requestid = ================================================ FILE: docs/content/ref/proxy.header.sts.maxage.md ================================================ --- title: "proxy.header.sts.maxage" --- `proxy.header.sts.maxage` enables and configures the max-age of HSTS for TLS requests. When set greater than zero this enables the Strict-Transport-Security header and sets the max-age value in the header. The default is proxy.header.sts.maxage = 0 ================================================ FILE: docs/content/ref/proxy.header.sts.preload.md ================================================ --- title: "proxy.header.sts.preload" --- `proxy.header.sts.preload` instructs HSTS to include the preload directive. When set to true, the 'preload' option will be added to the Strict-Transport-Security header. Sending the preload directive from your site can have PERMANENT CONSEQUENCES and prevent users from accessing your site and any of its subdomains if you find you need to switch back to HTTP. Please read the details at [https://hstspreload.org/#removal](https://hstspreload.org/#removal) before sending the header with "preload". The default is proxy.header.sts.preload = false ================================================ FILE: docs/content/ref/proxy.header.sts.subdomains.md ================================================ --- title: "proxy.header.sts.subdomains" --- `proxy.header.sts.subdomains` instructs HSTS to include subdomains. When set to true, the 'includeSubDomains' option will be added to the Strict-Transport-Security header. The default is proxy.header.sts.subdomains = false ================================================ FILE: docs/content/ref/proxy.header.tls.md ================================================ --- title: "proxy.header.tls" --- `proxy.header.tls` configures the header to set for TLS connections. When set to a non-empty value the proxy will set this header on every TLS request to the value of [proxy.header.tls.value](/ref/proxy.header.tls.value/) The default is proxy.header.tls = ================================================ FILE: docs/content/ref/proxy.header.tls.value.md ================================================ --- title: "proxy.header.tls.value" --- `proxy.header.tls.value` configures the value to set the [proxy.header.tls](/ref/proxy.header.tls/) header to for TLS connections. The default is proxy.header.tls.value = ================================================ FILE: docs/content/ref/proxy.idleconntimeout.md ================================================ `proxy.idleconntimeout` configures idle connection timeout, which influences when to close keep-alive connections. The default is proxy.idleconntimeout = 15s ================================================ FILE: docs/content/ref/proxy.keepalivetimeout.md ================================================ `proxy.keepalivetimeout` configures the keep-alive timeout. This configures the KeepAliveTimeout of the network dialer. The default is proxy.keepalivetimeout = 0s ================================================ FILE: docs/content/ref/proxy.localip.md ================================================ --- title: "proxy.localip" --- `proxy.localip` configures the ip address of the proxy which is added to the Header configured by [`proxy.header.clientip`](/ref/proxy.header.clientip/) and to the `Forwarded: by=` attribute. The local non-loopback address is detected during startup but can be overwritten with this property. The default is proxy.localip = ================================================ FILE: docs/content/ref/proxy.matcher.md ================================================ --- title: "proxy.matcher" --- `proxy.matcher` configures the path matching algorithm. * `prefix`: prefix matching * `iprefix`: case insensitive prefix matching * `glob`: glob matching When `prefix` matching is enabled then the route path must be a prefix of the request URI, e.g. `/foo` matches `/foo`, `/foot` but not `/fo`. When `glob` matching is enabled the route is evaluated according to globbing rules provided by the Go [`path.Match`](https://golang.org/pkg/path/#Match) function. For example, `/foo*` matches `/foo`, `/fool` and `/fools`. Also, `/foo/*/bar` matches `/foo/x/bar`. `iprefix` matching is similar to `prefix`, except it uses a case insensitive comparison The default is proxy.matcher = prefix ================================================ FILE: docs/content/ref/proxy.maxconn.md ================================================ --- title: "proxy.maxconn" --- `proxy.maxconn` configures the maximum number of cached incoming and outgoing connections. This configures the [MaxIdleConnsPerHost](https://golang.org/pkg/net/http/#Transport.MaxIdleConnsPerHost) of the [http.Transport](https://golang.org/pkg/net/http/#Transport). The default is proxy.maxconn = 10000 ================================================ FILE: docs/content/ref/proxy.noroutestatus.md ================================================ --- title: "proxy.noroutestatus" --- `proxy.noroutestatus` configures the response code when no route was found. The default is proxy.noroutestatus = 404 ================================================ FILE: docs/content/ref/proxy.responseheadertimeout.md ================================================ --- title: "proxy.responseheadertimeout" --- `proxy.responseheadertimeout` configures the [ResponseHeaderTimeout](https://golang.org/pkg/net/http/#Transport.ResponseHeaderTimeout) of the [http.Transport](https://golang.org/pkg/net/http/#Transport). The default is proxy.responseheadertimeout = 0s ================================================ FILE: docs/content/ref/proxy.shutdownwait.md ================================================ --- title: "proxy.shutdownwait" --- `proxy.shutdownwait` configures the time for a graceful shutdown. After a signal is caught the proxy will immediately suspend routing traffic and respond with a `503 Service Unavailable` for the duration of the given period. The default is proxy.shutdownwait = 0s ================================================ FILE: docs/content/ref/proxy.strategy.md ================================================ --- title: "proxy.strategy" --- `proxy.strategy` configures the load balancing strategy. * `rnd`: pseudo-random distribution configures a pseudo-random distribution by using the microsecond fraction of the time of the request. * `rr`: round-robin distribution configures a round-robin distribution. The default is proxy.strategy = rnd ================================================ FILE: docs/content/ref/registry.backend.md ================================================ --- title: "registry.backend" --- `registry.backend` configures which backend is used. Supported backends are: `consul`, `static`, `file`, `custom`. If custom is used fabio makes an api call to a remote system expecting the below json response ```json [ { "cmd": "string", "service": "string", "src": "string", "dst": "string", "weight": float, "tags": ["string"], "opts": {"string":"string"} } ] ``` The default is registry.backend = consul Short description of the fields required for a custom backend To configure routes Fabio uses a Config Language, specified [here](https://fabiolb.net/cfg/) - cmd - the command to add, remove or change weight of a route. For example `route add` to add a new route mapping. - service - the name that the service will show up in the UI. - src - usually the prefix that will be used in the routing table. - dst - the endpoint that will be used as the destination of the routing table. - weight - defines the weight of this path to perform routing. For example route A 90% and route B 10% for canary deployments. - tags - a list of tags, provide a way to filter routes, making it easier to do operations like bulk deletes `route del tags "dev"`. - opts - a KV map of the config language list of options. for example `proto` or `prefix` ================================================ FILE: docs/content/ref/registry.consul.addr.md ================================================ --- title: "registry.consul.addr" --- `registry.consul.addr` configures the address of the Consul agent to connect to. The default is registry.consul.addr = localhost:8500 ================================================ FILE: docs/content/ref/registry.consul.checksRequired.md ================================================ --- title: "registry.consul.checksRequired" --- `registry.consul.checksRequired` configures how many health checks must pass in order for fabio to consider a service available. Possible values are: * `one`: at least one health check must pass * `all`: all health checks must pass The default is registry.consul.checksRequired = one ================================================ FILE: docs/content/ref/registry.consul.kvpath.md ================================================ --- title: "registry.consul.kvpath" --- `registry.consul.kvpath` configures the KV path for manual routes. The Consul KV path is watched for changes which get appended to the routing table. This allows for manual overrides and weighted round-robin routes. As of version 1.5.7 fabio will treat the kv path as a prefix and combine the values of the key itself and all its subkeys in alphabetical order. To see all updates you may want to set [`-log.routes.format`](/ref/log.routes.format/) to `all`. You can modify the content of the routes with the `consul` tool or via the [Consul API](https://www.consul.io/api/index.html): ``` consul kv put fabio/config "route add svc /maint http://5.6.7.8:5000\nroute add svc / http://1.2.3.4:5000\n" # fabio >= 1.5.7 supports prefix match consul kv put fabio/config/maint "route add svc /maint http://5.6.7.8:5000" consul kv put fabio/config/catchall "route add svc / http://1.2.3.4:5000" consul kv delete fabio/config/maint ``` The default is registry.consul.kvpath = /fabio/config ================================================ FILE: docs/content/ref/registry.consul.namespace.md ================================================ --- title: "registry.consul.namespace" --- `registry.consul.namespace` configures the consul namespace in which fabio will register itself. Namespaces are a feature only available in the enterprise version of consul. In the open-source version or with an empty namespace option fabio will be registered in the default namespace. Only services running in the same consul namespace will be picked up by fabio. The default is registry.consul.namespace = ================================================ FILE: docs/content/ref/registry.consul.noroutehtmlpath.md ================================================ --- title: "registry.consul.noroutehtmlpath" --- `registry.consul.noroutehtmlpath` configures the KV path for the HTML page when no route was found. The consul KV path is watched for changes. The default is registry.consul.noroutehtmlpath = /fabio/noroute.html ================================================ FILE: docs/content/ref/registry.consul.pollInterval.md ================================================ --- title: "registry.consul.pollInterval" --- `registry.consul.pollInterval` configures the poll interval for route updates. If Poll interval is set to 0 the updates will be disabled and fall back to blocking queries. Other values can be any time definition. e.g. `1s, 100ms` The default is registry.consul.pollInterval = 0 ================================================ FILE: docs/content/ref/registry.consul.register.addr.md ================================================ --- title: "registry.consul.register.addr" --- `registry.consul.register.addr` configures the address for the service registration. Fabio registers itself in consul with this `host:port` address. It must point to the UI/API endpoint configured by [ui.addr](/ref/ui.addr/) and defaults to its value. The default is registry.consul.register.addr = :9998 ================================================ FILE: docs/content/ref/registry.consul.register.checkInterval.md ================================================ --- title: "registry.consul.register.checkInterval" --- `registry.consul.register.checkInterval` configures the interval for the health check. Fabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health and this value tells consul how often to check it. The default is registry.consul.register.checkInterval = 1s ================================================ FILE: docs/content/ref/registry.consul.register.checkTLSSkipVerify.md ================================================ --- title: "registry.consul.register.checkTLSSkipVerify" --- `registry.consul.register.checkTLSSkipVerify` configures TLS verification for the health check. Fabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health and this value tells consul to skip TLS certificate validation for https checks. The default is registry.consul.register.checkTLSSkipVerify = false ================================================ FILE: docs/content/ref/registry.consul.register.checkTimeout.md ================================================ --- title: "registry.consul.register.checkTimeout" --- `registry.consul.register.checkTimeout` configures the timeout for the health check. Fabio registers an http health check on http(s)://[ui.addr](/ref/ui.addr)/health and this value tells Consul how long to wait for a response. The default is registry.consul.register.checkTimeout = 3s ================================================ FILE: docs/content/ref/registry.consul.register.deregisterCriticalServiceAfter.md ================================================ --- title: "registry.consul.register.deregisterCriticalServiceAfter" --- This option is deprecated and has no effect in versions after 1.5.11. Services are now always deregistered shortly after fabio exits for any reason. In versions up to and including 1.5.11 `registry.consul.register.deregisterCriticalServiceAfter` configures the duration after which registered services are removed from Consul after fabio exits abruptly (services are always deregistered immediately when fabio exits normally). At the time of this writing, Consul enforces a minimum value of one minute and runs its reaper process every thirty seconds. The default for fabio <= 1.5.11 is registry.consul.register.deregisterCriticalServiceAfter = 90m ================================================ FILE: docs/content/ref/registry.consul.register.enabled.md ================================================ --- title: "registry.consul.register.enabled" --- `registry.consul.register.enabled` configures whether fabio registers itself in Consul. Fabio will register itself in consul only if this value is set to `true` which is the default. To disable registration set it to any other value, e.g. `false` The default is registry.consul.register.enabled = true ================================================ FILE: docs/content/ref/registry.consul.register.name.md ================================================ --- title: "registry.consul.register.name" --- `registry.consul.register.name` configures the name for the service registration. Fabio registers itself in consul under this service name. The default is registry.consul.register.name = fabio ================================================ FILE: docs/content/ref/registry.consul.register.tags.md ================================================ --- title: "registry.consul.register.tags" --- `registry.consul.register.tags` configures the tags for the service registration. Fabio registers itself with these tags. You can provide a comma separated list of tags. The default is registry.consul.register.tags = ================================================ FILE: docs/content/ref/registry.consul.service.status.md ================================================ --- title: "registry.consul.service.status" --- `registry.consul.service.status` configures the valid service status values for services included in the routing table. The values are a comma separated list of `passing`, `warning`, `critical` and `unknown` The default is registry.consul.service.status = passing ================================================ FILE: docs/content/ref/registry.consul.serviceMonitors.md ================================================ --- title: "registry.consul.serviceMonitors" --- `registry.consul.serviceMonitors` configures the concurrency for route updates. Fabio will make up to the configured number of concurrent calls to Consul to fetch status data for route updates. The default is registry.consul.serviceMonitors = 1 ================================================ FILE: docs/content/ref/registry.consul.tagprefix.md ================================================ --- title: "registry.consul.tagprefix" --- `registry.consul.tagprefix` configures the prefix for tags which define routes. Services which define routes publish one or more tags with host/path routes which they serve. These tags must have this prefix to be recognized as routes. The default is registry.consul.tagprefix = urlprefix- ================================================ FILE: docs/content/ref/registry.consul.token.md ================================================ --- title: "registry.consul.token" --- `registry.consul.token` configures the acl token for consul. The default is registry.consul.token = ================================================ FILE: docs/content/ref/registry.custom.checkTLSSkipVerify.md ================================================ --- title: "registry.custom.checkTLSSkipVerify" --- `registry.custom.checkTLSSkipVerify` disables the TLS validation for the API call The default is registry.custom.checkTLSSkipVerify = false ================================================ FILE: docs/content/ref/registry.custom.host.md ================================================ --- title: "registry.custom.host" --- `registry.custom.host` configures the host:port for fabio to make the API call The default is registry.custom.host = ================================================ FILE: docs/content/ref/registry.custom.path.md ================================================ --- title: "registry.custom.path" --- `registry.custom.path` is the path used in the custom back end API Call The path does not need to contain the initial '/' Example: registry.custom.path = api/v1/ The default is registry.custom.path = ================================================ FILE: docs/content/ref/registry.custom.pollinginterval.md ================================================ --- title: "registry.custom.pollinterval" --- `registry.custom.pollinterval` is the length of time between API calls The default is registry.custom.pollinterval = 10s ================================================ FILE: docs/content/ref/registry.custom.queryparams.md ================================================ --- title: "registry.custom.queryparams" --- `registry.custom.queryparams` is the query parameters used in the custom back end API Call Multiple query parameters should be separated with an & Example: registry.custom.queryparams = foo=bar&bar=foo The default is registry.custom.queryparams = ================================================ FILE: docs/content/ref/registry.custom.scheme.md ================================================ --- title: "registry.custom.scheme" --- `registry.custom.scheme` configures the scheme use to make the API call must be one of `http`, `https` The default is registry.custom.scheme = https ================================================ FILE: docs/content/ref/registry.custom.timeout.md ================================================ --- title: "registry.custom.timeout" --- `registry.custom.timeout` controls the timeout for the API call The default is registry.custom.timeout = 5s ================================================ FILE: docs/content/ref/registry.file.noroutehtmlpath.md ================================================ --- title: "registry.file.noroutehtmlpath" --- `registry.file.noroutehtmlpath` configures the path the HTML page when no route was found. The default is registry.file.noroutehtmlpath = ================================================ FILE: docs/content/ref/registry.file.path.md ================================================ --- title: "registry.file.path" --- `registry.file.path` configures a file based routing table. The value configures the path to the file with the routing table. The default is registry.file.path = ================================================ FILE: docs/content/ref/registry.retry.md ================================================ --- title: "registry.retry" --- `registry.retry` configures the interval with which fabio tries to connect to the registry during startup. The default is registry.retry = 500ms ================================================ FILE: docs/content/ref/registry.static.noroutehtmlpath.md ================================================ --- title: "registry.static.noroutehtml" --- `registry.static.noroutehtml` configures the the HTML for the page when no route was found. The default is registry.static.noroutehtmlpath = ================================================ FILE: docs/content/ref/registry.static.routes.md ================================================ --- title: "registry.static.routes" --- `registry.static.routes` configures a static routing table. #### Example registry.static.routes = \ route add svc / http://1.2.3.4:5000/ The default is registry.static.routes = ================================================ FILE: docs/content/ref/registry.timeout.md ================================================ --- title: "registry.timeout" --- `registry.timeout` configures how long fabio tries to connect to the registry backend during startup. The default is registry.timeout = 10s ================================================ FILE: docs/content/ref/runtime.gogc.md ================================================ --- title: "runtime.gogc" --- `runtime.gogc` configures GOGC (the GC target percentage). Setting `runtime.gogc` is equivalent to setting the `GOGC` environment variable which also takes precedence over the value from the config file. Increasing this value means fewer but longer GC cycles since there is more garbage to collect. NOTE - the default for fabio up to 1.5.14 was 800. This changed to 100 in version 1.5.15 The default is runtime.gogc = 100 ================================================ FILE: docs/content/ref/runtime.gomaxprocs.md ================================================ --- title: "runtime.gomaxprocs" --- `runtime.gomaxprocs` configures GOMAXPROCS. Setting `runtime.gomaxprocs` is equivalent to setting the `GOMAXPROCS` environment variable which also takes precedence over the value from the config file. If `runtime.gomaxprocs` < 0 then all CPU cores are used. The default is runtime.gomaxprocs = -1 ================================================ FILE: docs/content/ref/ui.access.md ================================================ --- title: "ui.access" --- `ui.access` configures the access mode for the UI. * `ro`: read-only access * `rw`: read-write access The default is ui.access = rw ================================================ FILE: docs/content/ref/ui.addr.md ================================================ --- title: "ui.addr" --- `ui.addr` configures the address the UI is listening on. The listener uses the same syntax as [proxy.addr](/ref/proxy.addr/) but supports only a single listener. To enable HTTPS configure a certificate source. You should use a different certificate source than the one you use for the external connections, e.g. `cs=ui`. The default is ui.addr = :9998 ================================================ FILE: docs/content/ref/ui.color.md ================================================ --- title: "ui.color" --- `ui.color` configures the background color of the UI. Color names are from http://materializecss.com/color.html The default is ui.color = light-green ================================================ FILE: docs/content/ref/ui.routingtable.source.host.md ================================================ --- title: "ui.routingtable.source.host" --- `ui.routingtable.source.host` configures an optional host or base address for the link in the source column. This 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. If this is not set, and the source link is enabled, the link will default to current host. This is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true. The default is ui.routingtable.source.host = ================================================ FILE: docs/content/ref/ui.routingtable.source.linkenabled.md ================================================ --- title: "ui.routingtable.source.linkenabled" --- `ui.routingtable.source.linkenabled` optionally configures if the routing table's column "source" should be a clickable link. The default is ui.routingtable.source.linkenabled = false ================================================ FILE: docs/content/ref/ui.routingtable.source.newtab.md ================================================ --- title: "ui.routingtable.source.newtab" --- `ui.routingtable.source.newtab` configures if the source link should open in a new tab. This is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true. The default is ui.routingtable.source.newtab = true ================================================ FILE: docs/content/ref/ui.routingtable.source.port.md ================================================ --- title: "ui.routingtable.source.port" --- `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/). If 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). This is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true. The default is ui.routingtable.source.port = ================================================ FILE: docs/content/ref/ui.routingtable.source.scheme.md ================================================ --- title: "ui.routingtable.source.scheme" --- `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. This is only applicable if the [linkenabled](/ref/ui.routingtable.source.linkenabled/) is set to true. The default is ui.routingtable.source.scheme = http ================================================ FILE: docs/content/ref/ui.title.md ================================================ --- title: "ui.title" --- `ui.title` configures an optional title for the UI. The default is ui.title = ================================================ FILE: docs/layouts/404.html ================================================ {{partial "header" .}}
{{partial "sidebar" .}}
{{partial "footer" .}} ================================================ FILE: docs/layouts/_default/section.html ================================================ {{partial "header" .}}
{{partial "sidebar" .}}

{{.Title}}

{{if .Params.since}}

{{.Params.since}}

{{end}} {{.Content}}
{{partial "footer" .}} ================================================ FILE: docs/layouts/_default/single.html ================================================ {{partial "header" .}}
{{partial "sidebar" .}}
{{$section := .}}

{{.Title}}

{{.Content}} {{range .Pages}}

{{.Title}} ^

{{if .Params.since}}

{{.Params.since}}

{{end}} {{.Content}} {{end}}
{{partial "footer" .}} ================================================ FILE: docs/layouts/index.html ================================================ {{partial "header" .}}
{{partial "sidebar" .}}

{{.Title}}

{{.Content}}
{{partial "footer" .}} ================================================ FILE: docs/layouts/partials/footer.html ================================================

Copyright © 2020-{{ now.Format "2006" }} Education Networks of America. All rights reserved.
Copyright © 2017-2020 Frank Schröder. All rights reserved.
Copyright © 2015-2017 eBay Software Foundation. All rights reserved
Last updated on {{ now.Format "2 Jan 2006 15:04"}}

================================================ FILE: docs/layouts/partials/header.html ================================================ {{.Title}} ================================================ FILE: docs/layouts/partials/sidebar.html ================================================ {{$currentPage := .}} ================================================ FILE: docs/static/CNAME ================================================ fabiolb.net ================================================ FILE: docs/static/check/ok ================================================ ok ================================================ FILE: docs/static/css/custom.css ================================================ /* workaround for firefox to display the logo */ .site-header .navbar-brand > img { height: 100%; } /* set navbar to static for now. If JS is enabled, we will revert to fixed in * custom.js, but if JS isn't enabled a fixed header overlaps the content. */ .site-header.sticky .navbar { position: static; } article.main-content { padding-top: 0; } body, h1,h2,h3,h4,h5,h6 { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } h2 a:after { background-color: transparent; } .banner.auto-size { background-color: #62a8ea; padding-top: 40px; } p.since::before { content: "Since "; } p.since { font-size: 16px; } /* adjustment to code example sections so they are more readable */ pre[class*="language"], pre code[class*="language"] { text-shadow: none; color: #808080; } pre code .token.operator, pre code[class*="operator"] { background: inherit; } .clipboard-copy { top: 6px !important; right: 8px; color: #808080; background-color: transparent; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; opacity: 1; padding: 2px 5px; } pre .language-name { right: 75px; } ================================================ FILE: docs/static/img/manifest.json ================================================ { "name": "", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: docs/static/js/custom.js ================================================ $(function() { // Make the navbar only fixed if JS is enabled, else it overlaps the content. $(".site-header.sticky .navbar").css('position', 'fixed'); }); ================================================ FILE: exit/listen.go ================================================ // Package exit allows to register callbacks which are called on program exit. package exit import ( "log" "os" "os/signal" "sync" "syscall" ) var wg sync.WaitGroup // quit channel is closed to cleanup exit listeners. var quit = make(chan bool) // Listen registers an exit handler which is called on // SIGINT/SIGTERM or when Exit/Fatal/Fatalf is called. // SIGHUP is ignored since that is usually used for // triggering a reload of configuration which isn't // supported but shouldn't kill the process either. func Listen(fn func(os.Signal)) { wg.Add(1) go func() { defer wg.Done() for { sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) var sig os.Signal select { case sig = <-sigchan: switch sig { case syscall.SIGHUP: log.Print("[INFO] Caught SIGHUP. Ignoring") continue case os.Interrupt: log.Print("[INFO] Caught SIGINT. Exiting") case syscall.SIGTERM: log.Print("[INFO] Caught SIGTERM. Exiting") default: // fallthrough in case we forgot to add a switch clause. log.Printf("[INFO] Caught signal %v. Exiting", sig) } case <-quit: } if fn != nil { fn(sig) } return } }() } // stubbed out for testing var osExit = os.Exit // Exit terminates the application via os.Exit but // waits for all exit handlers to complete before // calling os.Exit. func Exit(code int) { defer func() { recover() }() // don't panic if close(quit) is called concurrently close(quit) wg.Wait() osExit(code) } // Fatal is a replacement for log.Fatal which will trigger // the closure of all registered exit handlers and waits // for their completion and then call os.Exit(1). func Fatal(v ...interface{}) { log.Print(v...) Exit(1) } // Fatalf is a replacement for log.Fatalf and behaves like Fatal. func Fatalf(format string, v ...interface{}) { log.Printf(format, v...) Exit(1) } // Wait waits for all exit handlers to complete. func Wait() { wg.Wait() } ================================================ FILE: exit/listen_test.go ================================================ package exit import ( "bytes" "log" "os" "strings" "testing" ) func TestExit(t *testing.T) { var b bytes.Buffer var exitCode int var sig1, sig2 bool flags := log.Flags() log.SetFlags(0) log.SetOutput(&b) defer func() { log.SetOutput(os.Stderr) log.SetFlags(flags) }() osExit = func(code int) { exitCode = code } defer func() { osExit = os.Exit }() Listen(func(os.Signal) { sig1 = true }) Listen(func(os.Signal) { sig2 = true }) // trigger a concurrent exit via fatal/fatalf // it is not guaranteed that any log output is written // before the application exits. This is only to test // that two go routines can call Fatal without causing a // panic. go Fatal("a") go Fatalf("b") // wait for listeners to return Wait() out := b.String() if !strings.Contains("a\n b\n a\nb\n b\na\n", out) { t.Errorf("log.Fatal did not happen: %q", out) } if exitCode != 1 { t.Errorf("os.Exit not called") } if !sig1 || !sig2 { t.Errorf("signal handlers not completed") } } ================================================ FILE: fabio.iml ================================================ ================================================ FILE: fabio.properties ================================================ # proxy.cs configures one or more certificate sources. # # Each certificate source is configured with a list of # key/value options. Each source must have a unique # name which can then be referred to in a listener # configuration. # # cs=;type=;opt=arg;opt[=arg];... # # All certificates need to be provided in PEM format. # # The following types of certificate sources are available: # # File # # The file certificate source supports one certificate which is loaded at # startup and is cached until the service exits. # # The 'cert' option contains the path to the certificate file. The 'key' # option contains the path to the private key file. If the certificate file # contains both the certificate and the private key the 'key' option can be # omitted. The 'clientca' option contains the path to one or more client # authentication certificates. # # cs=;type=file;cert=p/a-cert.pem;key=p/a-key.pem;clientca=p/clientAuth.pem # # Path # # The path certificate source loads certificates from a directory in # alphabetical order and refreshes them periodically. # # The 'cert' option provides the path to the TLS certificates and the # 'clientca' option provides the path to the certificates for client # authentication. # # TLS certificates are stored either in one or two files: # # www.example.com.pem or www.example.com-{cert,key}.pem # # TLS certificates are loaded in alphabetical order and the first certificate # is the default for clients which do not support SNI. # # The 'refresh' option can be set to specify the refresh interval for the TLS # certificates. Client authentication certificates cannot be refreshed since # Go does not provide a mechanism for that yet. # # The default refresh interval is 3 seconds and cannot be lower than 1 second # to prevent busy loops. To load the certificates only once and disable # automatic refreshing set 'refresh' to zero. # # cs=;type=path;cert=path/to/certs;clientca=path/to/clientcas;refresh=3s # # HTTP # # The http certificate source loads certificates from an HTTP/HTTPS server. # # The 'cert' option provides a URL to a text file which contains all files # that should be loaded from this directory. The filenames follow the same # rules as for the path source. The text file can be generated with: # # ls -1 *.pem > list # # The 'clientca' option provides a URL for the client authentication # certificates analogous to the 'cert' option. # # Authentication credentials can be provided in the URL as request parameter, # as basic authentication parameters or through a header. # # The 'refresh' option can be set to specify the refresh interval for the TLS # certificates. Client authentication certificates cannot be refreshed since # Go does not provide a mechanism for that yet. # # The default refresh interval is 3 seconds and cannot be lower than 1 second # to prevent busy loops. To load the certificates only once and disable # automatic refreshing set 'refresh' to zero. # # cs=;type=http;cert=https://host.com/path/to/cert/list&token=123 # cs=;type=http;cert=https://user:pass@host.com/path/to/cert/list # cs=;type=http;cert=https://host.com/path/to/cert/list;hdr=Authorization: Bearer 1234 # # Consul # # The consul certificate source loads certificates from consul. # # The 'cert' option provides a KV store URL where the the TLS certificates are # stored. # # The 'clientca' option provides a URL to a path in the KV store where the the # client authentication certificates are stored. # # The filenames follow the same rules as for the path source. # # The TLS certificates are updated automatically whenever the KV store # changes. The client authentication certificates cannot be updated # automatically since Go does not provide a mechanism for that yet. # # cs=;type=consul;cert=http://localhost:8500/v1/kv/path/to/cert&token=123 # # Vault # # The Vault certificate store uses HashiCorp Vault as the certificate # store. # # The 'cert' option provides the path to the TLS certificates and the # 'clientca' option provides the path to the certificates for client # authentication. # # The 'refresh' option can be set to specify the refresh interval for the TLS # certificates. Client authentication certificates cannot be refreshed since # Go does not provide a mechanism for that yet. # # The default refresh interval is 3 seconds and cannot be lower than 1 second # to prevent busy loops. To load the certificates only once and disable # automatic refreshing set 'refresh' to zero. # # The path to vault must be provided in the VAULT_ADDR environment # variable. The token can be provided in the VAULT_TOKEN environment # variable, or provided by using the Vault fetch token option. By default the # token is loaded once from the VAULT_TOKEN environment variable. See Vault PKI for details. # # cs=;type=vault;cert=secret/fabio/certs # # Vault PKI # # The Vault PKI certificate store uses HashiCorp Vault's PKI backend to issue # certificates on-demand. # # The 'cert' option provides a PKI backend path for issuing certificates. The # 'clientca' option works in the same way as for the generic Vault source. # # The 'refresh' option determines how long before the expiration date # certificates are re-issued. Values smaller than one hour are silently changed # to one hour, which is also the default. # # cs=;type=vault-pki;cert=pki/issue/example-dot-com;refresh=24h;clientca=secret/fabio/client-certs # # This source will issue server certificates on-demand using the PKI backend # and re-issue them 24 hours before they expire. The CA for client # authentication is expected to be stored at secret/fabio/client-certs. # # 'vaultfetchtoken' enables fetching the vault token from a file on the filesystem or an environment # variable at the Vault refresh interval. If fetching the token from a file the 'file:[path]' syntax should be used, # if fetching the token from an env variable, the 'env:[ENV]' syntax should be used. # # cs=;type=vault;cert=secret/fabio/certs;vaultfetchtoken=env:VAULT_TOKEN # # Common options # # All certificate stores support the following options: # # caupgcn: Upgrade a self-signed client auth certificate with this common-name # to a CA certificate. Typically used for self-singed certificates # for the Amazon AWS Api Gateway certificates which do not have the # CA flag set which makes them unsuitable for client certificate # authentication in Go. For the AWS Api Gateway set this value # to 'ApiGateway' to allow client certificate authentication. # This replaces the deprecated parameter 'aws.apigw.cert.cn' # which was introduced in version 1.1.5. # # Examples: # # # file based certificate source # proxy.cs = cs=some-name;type=file;cert=p/a-cert.pem;key=p/a-key.pem # # # path based certificate source # proxy.cs = cs=some-name;type=path;path=path/to/certs # # # HTTP certificate source # proxy.cs = cs=some-name;type=http;cert=https://user:pass@host:port/path/to/certs # # # Consul certificate source # proxy.cs = cs=some-name;type=consul;cert=https://host:port/v1/kv/path/to/certs?token=abc123 # # # Vault certificate source # proxy.cs = cs=some-name;type=vault;cert=secret/fabio/certs # # # Vault PKI certificate source # proxy.cs = cs=some-name;type=vault-pki;cert=pki/issue/example-dot-com # # # Multiple certificate sources # proxy.cs = cs=srcA;type=path;path=path/to/certs,\ # cs=srcB;type=http;cert=https://user:pass@host:port/path/to/certs # # # path based certificate source for AWS Api Gateway # proxy.cs = cs=some-name;type=path;path=path/to/certs;clientca=path/to/clientcas;caupgcn=ApiGateway # # The default is # # proxy.cs = # proxy.addr configures listeners. # # Each listener is configured with and address and a # list of optional arguments in the form of # # [host]:port;opt=arg;opt[=arg];... # # Each listener has a protocol which is configured # with the 'proto' option for which it routes and # forwards traffic. # # The supported protocols are: # # * http for HTTP based protocols # * https for HTTPS based protocols # * tcp for a raw TCP proxy with or witout TLS support # * tcp+sni for an SNI aware TCP proxy # * tcp-dynamic for a consul driven TCP proxy # * https+tcp+sni for an SNI aware TCP proxy with https fallthrough # * prometheus for a prometheus listener. use this with the prometheus metrics target. # # If no 'proto' option is specified then the protocol # is either 'http' or 'https' depending on whether a # certificate source is configured via the 'cs' option # which contains the name of the certificate source. # # The TCP+SNI proxy analyzes the ClientHello message # of TLS connections to extract the server name # extension and then forwards the encrypted traffic # to the destination without decrypting the traffic. # # General options: # # rt: Sets the read timeout as a duration value (e.g. '3s') # # wt: Sets the write timeout as a duration value (e.g. '3s') # # it: Sets the idle timeout as a duration value (e.g. '3s') # # strictmatch: When set to 'true' the certificate source must provide # a certificate that matches the hostname for the connection # to be established. Otherwise, the first certificate is used # if no matching certificate was found. This matches the default # behavior of the Go TLS server implementation. # # pxyproto: When set to 'true' the listener will respect upstream v1 # PROXY protocol headers. # NOTE: PROXY protocol was on by default from 1.1.3 to 1.5.10. # This changed to off when this option was introduced with # the 1.5.11 release. # For more information about the PROXY protocol, please see: # http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt # # pxytimeout: Sets PROXY protocol header read timeout as a duration (e.g. '250ms'). # This defaults to 250ms if not set when 'pxyproto' is enabled. # # refresh: Sets the refresh interval to check the route table for updates. # Used when 'tcp-dynamic' is enabled. # # TLS options: # # tlsmin: Sets the minimum TLS version for the handshake. This value # is one of [ssl30, tls10, tls11, tls12] or the corresponding # version number from https://golang.org/pkg/crypto/tls/#pkg-constants # # tlsmax: Sets the maximum TLS version for the handshake. See 'tlsmin' # for the format. # # tlsciphers: Sets the list of allowed ciphers for the handshake. The value # is a quoted comma-separated list of the hex cipher values or # the constant names from https://golang.org/pkg/crypto/tls/#pkg-constants, # e.g. "0xc00a,0xc02b" or "TLS_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_128_CBC_SHA" # # Examples: # # # HTTP listener on port 9999 # proxy.addr = :9999 # # # HTTP listener on IPv4 with read timeout # proxy.addr = 1.2.3.4:9999;rt=3s # # # HTTP listener on IPv6 with write timeout # proxy.addr = [2001:DB8::A/32]:9999;wt=5s # # # Multiple listeners # proxy.addr = 1.2.3.4:9999;rt=3s,[2001:DB8::A/32]:9999;wt=5s # # # HTTPS listener on port 443 with certificate source # proxy.addr = :443;cs=some-name # # # HTTPS listener on port 443 with certificate source and TLS options # proxy.addr = :443;cs=some-name;tlsmin=tls10;tlsmax=tls11;tlsciphers="0xc00a,0xc02b" # # # TCP listener on port 1234 with port routing # proxy.addr = :1234;proto=tcp # # # TCP listener on port 443 with SNI routing # proxy.addr = :443;proto=tcp+sni # # # TCP listener on port 443 with SNI routing with HTTPS fallthrough # proxy.addr = :443;proto=https+tcp+sni;cs=some-name # # # TCP listeners using consul for config with 5 second refresh interval # proxy.addr = 0.0.0.0:0;proto=tcp-dynamic;refresh=5s # # # prometheus listener. can optionally be used with cs= as well for TLS support. # proxy.addr = :9090;proto=prometheus;cs=some-name # # The default is # # proxy.addr = :9999 # proxy.localip configures the ip address of the proxy which is added # to the Header configured by proxy.header.clientip and to the 'Forwarded: by=' attribute. # # The local non-loopback address is detected during startup # but can be overwritten with this property. # # The default is # # proxy.localip = # proxy.strategy configures the load balancing strategy. # # rnd: pseudo-random distribution # rr: round-robin distribution # # "rnd" configures a pseudo-random distribution by using the microsecond # fraction of the time of the request. # # "rr" configures a round-robin distribution. # # The default is # # proxy.strategy = rnd # proxy.matcher configures the path matching algorithm. # # prefix: prefix matching # glob: glob matching # iprefix: case-insensitive prefix matching # # The default is # # proxy.matcher = prefix # proxy.noroutestatus configures the response code when no route was found. # # The default is # # proxy.noroutestatus = 404 # proxy.shutdownwait configures the time for a graceful shutdown. # # After a signal is caught the proxy will immediately suspend # routing traffic and respond with a 503 Service Unavailable # for the duration of the given period. # # The default is # # proxy.shutdownwait = 0s #proxy.deregistergraceperiod configures the time to wait before #shutting down the proxies de-registering from the service registry. # #After a signal is caught Fabio will immediately de-register from the #service registry and wait for `proxy.deregistergraceperiod` letting #in-flight requests finish after which it will continue with shutting #down the proxy. # #The default is # #proxy.deregistergraceperiod = 0s # proxy.responseheadertimeout configures the response header timeout. # # This configures the ResponseHeaderTimeout of the http.Transport. # # The default is # # proxy.responseheadertimeout = 0s # proxy.keepalivetimeout configures the keep-alive timeout. # # This configures the KeepAliveTimeout of the network dialer. # # The default is # # proxy.keepalivetimeout = 0s # proxy.idleconntimeout configures the idle connection timeout, when # to close (keep-alive) connections # # The default is # # proxy.idleconntimeout = 15s # proxy.dialtimeout configures the connection timeout for # outgoing connections. # # This configures the DialTimeout of the network dialer. # # The default is # # proxy.dialtimeout = 30s # proxy.flushinterval configures periodic flushing of the # response buffer for SSE (server-sent events) connections. # They are detected when the 'Accept' header is # 'text/event-stream'. # # The default is # # proxy.flushinterval = 1s # proxy.globalflushinterval configures periodic flushing of the # response buffer for non-SSE connections. By default it is not enabled. # # The default is # # proxy.globalflushinterval = 0 # proxy.maxconn configures the maximum number of cached # incoming and outgoing connections. # # This configures the MaxIdleConnsPerHost of the http.Transport. # # The default is # # proxy.maxconn = 10000 # proxy.header.clientip configures the header for the request ip. # # The remoteIP is taken from http.Request.RemoteAddr. # # The default is # # proxy.header.clientip = # proxy.header.tls configures the header to set for TLS connections. # # When set to a non-empty value the proxy will set this header on every # TLS request to the value of ${proxy.header.tls.value} # # The default is # # proxy.header.tls = # proxy.header.tls.value = # proxy.header.requestid configures the header for the adding a unique request id. # When set non-empty value the proxy will set this header on every request to the # unique UUID value. # # The default is # # proxy.header.requestid = # proxy.header.sts.maxage enables and configures the max-age of HSTS for TLS requests. # When set greater than zero this enables the Strict-Transport-Security header # and sets the max-age value in the header. # # The default is # # proxy.header.sts.maxage = 0 # proxy.header.sts.subdomains instructs HSTS to include subdomains. # When set to true, the 'includeSubDomains' option will be added to # the Strict-Transport-Security header. # # The default is # # proxy.header.sts.subdomains = false # proxy.header.sts.preload instructs HSTS to include the preload directive. # When set to true, the 'preload' option will be added to the # Strict-Transport-Security header. # # Sending the preload directive from your site can have PERMANENT CONSEQUENCES # and prevent users from accessing your site and any of its subdomains if you # find you need to switch back to HTTP. Please read the details at # https://hstspreload.org/#removal before sending the header with "preload". # # The default is # # proxy.header.sts.preload = false # proxy.gzip.contenttype configures which responses should be compressed. # # By default, responses sent to the client are not compressed even if the # client accepts compressed responses by setting the 'Accept-Encoding: gzip' # header. By setting this value responses are compressed if the Content-Type # header of the response matches and the response is not already compressed. # The list of compressable content types is defined as a regular expression. # The regular expression must follow the rules outlined in golang.org/pkg/regexp. # # A typical example is # # proxy.gzip.contenttype = ^(text/.*|application/(javascript|json|font-woff|xml)|.*\\+(json|xml))(;.*)?$ # # The default is # # proxy.gzip.contenttype = # proxy.auth configures one or more auth schemes. # # Each auth scheme is configured with a list of # key/value options. Each source must have a unique # name which can then be referred to in a routing # rule. # # name=;type=;opt=arg;opt[=arg];... # # The following types of auth schemes are available: # # Basic # # The basic auth scheme leverages http basic authentication using # one htpasswd file which is loaded at startup and by default is cached until # the service exits. However, it's possible to refresh htpasswd file # periodically by setting the refresh interval with 'refresh' option. # # The 'file' option contains the path to the htpasswd file. The 'realm' # option contains realm name (optional, default is the scheme 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. # # name=;type=basic;file=p/creds.htpasswd;realm=foo # # Examples # # # single basic auth scheme # # name=mybasicauth;type=basic;file=p/creds.htpasswd; # # # single basic auth scheme with refresh interval set to 30 seconds # # name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s # # # basic auth with multiple schemes # # proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd # name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm # # # proxy.grpcmaxrxmsgsize configures the grpc max receive message size in bytes. # The default is # proxy.grpcmaxrxmsgsize = 4194304 # # proxy.grpcmaxtxmsgsize configures the grpc max transmit messsage size in bytes # The default is # proxy.grpcmaxtxmsgsize = 4194304 # # # proxy.grpcshutdowntimeout configures the amount of time fabio will wait to attempt # to close the connection while waiting for grpc traffic to finish to a backend that's been # deregistered. Default value is # proxy.grpcshutdowntimeout = 2s # setting to 0s disables the wait. # log.access.format configures the format of the access log. # # If the value is either 'common' or 'combined' then the logs are written in # the Common Log Format or the Combined Log Format as defined below: # # 'common': $remote_host - - [$time_common] "$request" $response_status $response_body_size # 'combined': $remote_host - - [$time_common] "$request" $response_status $response_body_size "$header.Referer" "$header.User-Agent" # # Otherwise, the value is interpreted as a custom log format which is defined # with the following parameters. Providing an empty format when logging is # enabled is an error. To disable access logging leave the log.access.target # value empty. # # $header. - request http header (name: [a-zA-Z0-9-]+) # $remote_addr - host:port of remote client # $remote_host - host of remote client # $remote_port - port of remote client # $request - request # $request_args - request query parameters # $request_host - request host header (aka server name) # $request_method - request method # $request_scheme - request scheme # $request_uri - request URI # $request_url - request URL # $request_proto - request protocol # $response_body_size - response body size in bytes # $response_status - response status code # $response_time_ms - response time in S.sss format # $response_time_us - response time in S.ssssss format # $response_time_ns - response time in S.sssssssss format # $time_rfc3339 - log timestamp in YYYY-MM-DDTHH:MM:SSZ format # $time_rfc3339_ms - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format # $time_rfc3339_us - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format # $time_rfc3339_ns - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format # $time_unix_ms - log timestamp in unix epoch ms # $time_unix_us - log timestamp in unix epoch us # $time_unix_ns - log timestamp in unix epoch ns # $time_common - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ # $upstream_addr - host:port of upstream server # $upstream_host - host of upstream server # $upstream_port - port of upstream server # $upstream_request_scheme - upstream request scheme # $upstream_request_uri - upstream request URI # $upstream_request_url - upstream request URL # $upstream_service - name of the upstream service # # The default is # # log.access.format = common # log.access.target configures where the access log is written to. # # Options are 'stdout'. If the value is empty no access log is written. # # The default is # # log.access.target = # log.level configures the log level. # # Valid levels are TRACE, DEBUG, INFO, WARN, ERROR and FATAL. # # The default is # # log.level = INFO # log.routes.format configures the log output format of routing table updates. # # Changes to the routing table are written to the standard log. This option # configures the output format: # # detail: detailed routing table as ascii tree # delta: additions and deletions in config language # all: complete routing table in config language # # The default is # # log.routes.format = delta # registry.backend configures which backend is used. # Supported backends are: consul, static, file, custom # if custom is used fabio makes an api call to a remote system # expecting the below json response # [ # { # "cmd": "string", # "service": "string", # "src": "string", # "dst": "string", # "weight": float, # "tags": ["string"], # "opts": {"string":"string"} # } # ] # Short description of the fields required for a custom backend # # - cmd - the command to add, remove or change weight of a route. For example `route add` to add a new route mapping. # - service - the name that the service will show up in the UI. # - src - usually the prefix that will be used in the routing table. # - dst - the endpoint that will be used as the destination of the routing table. # - weight - defines the weight of this path to perform routing. For example route A 90% and route B 10% for canary deployments. # - tags - a list of tags, provide a way to filter routes, making it easier to do operations like bulk deletes `route del tags "dev"`. # - opts - a KV map of the config language list of options. for example `proto` or `prefix` # # The default is # # registry.backend = consul # registry.timeout configures how long fabio tries to connect to the registry # backend during startup. # # The default is # # registry.timeout = 10s # registry.retry configures the interval with which fabio tries to # connect to the registry during startup. # # The default is # # registry.retry = 500ms # registry.static.routes configures a static routing table. # # Example: # # registry.static.routes = \ # route add svc / http://1.2.3.4:5000/ # # The default is # # registry.static.routes = # registry.static.noroutehtmlpath configures the KV path for the HTML of the # noroutes page. # # The default is # # registry.static.noroutehtmlpath = # registry.file.path configures a file based routing table. # The value configures the path to the file with the routing table. # # The default is # # registry.file.path = # registry.file.noroutehtmlpath configures the KV path for the HTML of the # noroutes page. # # The default is # # registry.file.noroutehtmlpath = # registry.consul.addr configures the address of the consul agent to connect to. # # The default is # # registry.consul.addr = localhost:8500 # registry.consul.token configures the acl token for consul. # # The default is # # registry.consul.token = # registry.consul.tls.keyfile the path to the TLS certificate private key used for Consul communication. # # This is the full path to the TLS private key while using TLS transport to # communicate with Consul # # The default is # # registry.consul.tls.keyfile = # registry.consul.tls.certfile the path to the TLS certificate used for Consul communication. # # This is the full path to the TLS certificate while using TLS transport to # communicate with Consul # # The default is # # registry.consul.tls.certfile = # registry.consul.tls.cafile the path to the ca certificate used for Consul communication. # # This is the full path to the CA certificate while using TLS transport to # communicate with Consul # # The default is # # registry.consul.tls.cafile = # registry.consul.tls.capath the path to the folder containing CA certificates. # # This is the full path to the folder with CA certificates while using TLS transport to # communicate with Consul # # The default is # # registry.consul.tls.capath = # registry.consul.tls.insecureskipverify enable SSL verification with Consul. # # registry.consul.tls.insecureskipverify enables or disables SSL verification while using TLS transport to # communicate with Consul # # The default is # # registry.consul.tls.insecureskipverify = false # registry.consul.kvpath configures the KV path for manual routes. # # The consul KV path is watched for changes which get appended to # the routing table. This allows for manual overrides and weighted # round-robin routes. The key itself (e.g. fabio/config) and all # subkeys (e.g. fabio/config/foo and fabio/config/bar) are combined # in alphabetical order. # # The default is # # registry.consul.kvpath = /fabio/config # registry.consul.noroutehtmlpath configures the KV path for the HTML of the # noroutes page. # # The consul KV path is watched for changes. # # The default is # # registry.consul.noroutehtmlpath = /fabio/noroute.html # registry.consul.service.status configures the valid service status # values for services included in the routing table. # # The values are a comma separated list of # "passing", "warning", "critical" and "unknown" # # The default is # # registry.consul.service.status = passing # registry.consul.tagprefix configures the prefix for tags which define routes. # # Services which define routes publish one or more tags with host/path # routes which they serve. These tags must have this prefix to be # recognized as routes. # # The default is # # registry.consul.tagprefix = urlprefix- # registry.consul.register.enabled configures whether fabio registers itself in consul. # # Fabio will register itself in consul only if this value is set to "true" which # is the default. To disable registration set it to any other value, e.g. "false" # # The default is # # registry.consul.register.enabled = true # registry.consul.namespace configures the consul namespace in which fabio will register itself. # # Namespaces are a feature only available in the enterprise version of consul. In the open-source # version or with an empty namespace option fabio will be registered in the default namespace. Only # services running in the same consul namespace will be picked up by fabio. # # The default is # # registry.consul.namespace = # registry.consul.register.addr configures the address for the service registration. # # Fabio registers itself in consul with this host:port address. # It must point to the UI/API endpoint configured by ui.addr and defaults to its # value. # # The default is # # registry.consul.register.addr = :9998 # registry.consul.register.name configures the name for the service registration. # # Fabio registers itself in consul under this service name. # # The default is # # registry.consul.register.name = fabio # registry.consul.register.tags configures the tags for the service registration. # # Fabio registers itself with these tags. You can provide a comma separated list of tags. # # The default is # # registry.consul.register.tags = # registry.consul.register.checkInterval configures the interval for the health check. # # Fabio registers an http health check on http(s)://${ui.addr}/health # and this value tells consul how often to check it. # # The default is # # registry.consul.register.checkInterval = 1s # registry.consul.register.checkTimeout configures the timeout for the health check. # # Fabio registers an http health check on http(s)://${ui.addr}/health # and this value tells consul how long to wait for a response. # # The default is # # registry.consul.register.checkTimeout = 3s # registry.consul.register.checkTLSSkipVerify configures TLS verification for the health check. # # Fabio registers an http health check on http(s)://${ui.addr}/health # and this value tells consul to skip TLS certificate validation for # https checks. # # The default is # # registry.consul.register.checkTLSSkipVerify = false # registry.consul.register.checkDeregisterCriticalServiceAfter configures # automatic deregistration of a service after the health check is critical for # this length of time. # # Fabio registers an http health check on http(s)://${ui.addr}/health # and this value tells consul to deregister the associated service if the check # is critical for the specified duration. # # The default is # # registry.consul.register.checkDeregisterCriticalServiceAfter = 90m # registry.consul.checksRequired configures how many health checks # must pass in order for fabio to consider a service available. # # Possible values are: # one: at least one health check must pass # all: all health checks must pass # # The default is # # registry.consul.checksRequired = one # registry.consul.serviceMonitors configures the concurrency for # route updates. Fabio will make up to the configured number of # concurrent calls to Consul to fetch status data for route # updates. # # The default is # # registry.consul.serviceMonitors = 1 # registry.consul.pollInterval configures the poll interval # for route updates. If Poll interval is set to 0 the updates will # be disabled and fall back to blocking queries. Other values can # be any time definition. e.g. 1s, 100ms # # The default is # registry.consul.pollInterval = 0 # registry.custom.host configures the host:port for fabio to make the API call # # The default is # # registry.custom.host = # registry.custom.scheme configures the scheme use to make the API call # must be one of http, https # # The default is # # registry.custom.scheme = https # registry.custom.checkTLSSkipVerify disables the TLS validation for the API call # # The default is # # registry.custom.checkTLSSkipVerify = false # registry.custom.timeout controls the timeout for the API call # # The default is # # registry.custom.timeout = 5s # registry.custom.pollinterval is the length of time between API calls # # The default is # #registry.custom.pollinterval = 10s # registry.custom.path is the path used in the custom back end API Call # # The path does not need to contain the initial '/' # # Example: # # registry.custom.path = api/v1/ # # The default is # # registry.custom.path = # registry.custom.queryparams is the query parameters used in the custom back # end API Call # # Multiple query parameters should be separated with an & # # Example: # # registry.custom.queryparams = foo=bar&bar=foo # # The default is # # registry.custom.queryparams = # glob.matching.disabled disables glob matching on route lookups # If glob matching is enabled there is a performance decrease # for every route lookup. At a large number of services (> 500) this # can have a significant impact on performance. If glob matching is disabled # Fabio performs a static string compare for route lookups. # # The default is # # glob.matching.disabled = false # glob.cache.size sets the globCache size used for matching on route lookups. # # The default is # # glob.cache.size = 1000 # metrics.target configures the backend the metrics values are # sent to. # # Possible values are: # : do not report metrics # stdout: report metrics to stdout # graphite: report metrics to Graphite on ${metrics.graphite.addr} # statsd_raw: report metrics to StatsD on ${metrics.statsd.addr} # dogstatsd: report metrics to DogstatsD on ${metrics.dogstatsd.addr} # circonus: report metrics to Circonus (http://circonus.com/) # prometheus: report metrics on a prometheus listener. To combined with prometheus proxy.addr config # # The default is # # metrics.target = # note - multiple metrics targets can be defined separated by comma # metrics.prefix configures the template for the prefix of all reported metrics. # # Each metric has a unique name which is hard-coded to # # prefix.service.host.path.target-addr # # The value is expanded by the text/template package and provides # the following variables: # # - Hostname: the Hostname of the server # - Exec: the executable name of application # # The following additional functions are defined: # # - clean: lowercase value and replace '.' and ':' with '_' # # Template may include regular string parts to customize final prefix # # Example: # # Server hostname: test-001.something.com # Binary executable name: fabio # # The template variables are: # # .Hostname = test-001.something.com # .Exec = fabio # # which results to the following prefix string when using the # default template: # # test-001_something_com.fabio # # The default is # # metrics.prefix = {{clean .Hostname}}.{{clean .Exec}} # metrics.names configures the template for the route metric names # on backends that don't support tags. This is used in circonus, # graphite and statsd_raw. dogstatsd and prometheus ignore this. # The value is expanded by the text/template package and provides # the following variables: # # - Service: the service name # - Host: the host part of the URL prefix # - Path: the path part of the URL prefix # - TargetURL: the URL of the target # # The following additional functions are defined: # # - clean: lowercase value and replace '.' and ':' with '_' # # Given a route rule of # # route add testservice www.example.com/ http://10.1.2.3:12345/ # # the template variables are: # # .Service = testservice # .Host = www.example.com # .Path = / # .TargetURL.Host = 10.1.2.3:12345 # # which results to the following metric name when using the # default template: # # testservice.www_example_com./.10_1_2_3_12345 # # The default is # # metrics.names = {{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}} # metrics.interval configures the interval in which metrics are # reported. This has no effect on prometheus. # # The default is # # metrics.interval = 30s # metrics.timeout configures how long fabio tries to connect to the metrics # backend during startup. # # The default is # # metrics.timeout = 10s # metrics.retry configures the interval with which fabio tries to # connect to the metrics backend during startup. # # The default is # # metrics.retry = 500ms # metrics.graphite.addr configures the host:port of the Graphite # server. This is required when ${metrics.target} is set to "graphite". # # The default is # # metrics.graphite.addr = # metrics.statsd.addr configures the host:port of the StatsD # server. This is required when ${metrics.target} is set to "statsd_raw". # # The default is # # metrics.statsd.addr = # metrics.dogstatsd.addr configures the host:port of the DogStatsD # server. This is required when ${metrics.target} is set to "dogstatsd". # # The default is # # metrics.dogstatsd.addr = # metrics.circonus.apikey configures the API token key to use when # submitting metrics to Circonus. See: https://login.circonus.com/user/tokens # This is optional when ${metrics.target} is set to "circonus" but # ${metrics.circonus.submissionurl is specified}. # # The default is # # metrics.circonus.apikey = # metrics.circonus.submissionurl configures a specific check submission url # for a Check API object of a previously created HTTPTRAP check # This is optional when ${metrics.target} is set to "circonus" but # ${metrics.circonus.apikey is specified}. # #### Example # # `http://127.0.0.1:2609/write/fabio` # # The default is # # metrics.circonus.submissionurl = # metrics.circonus.apiapp configures the API token app to use when # submitting metrics to Circonus. See: https://login.circonus.com/user/tokens # This is optional when ${metrics.target} is set to "circonus". # # The default is # # metrics.circonus.apiapp = fabio # metrics.circonus.apiurl configures the API URL to use when # submitting metrics to Circonus. https://api.circonus.com/v2/ # will be used if no specific URL is provided. # This is optional when ${metrics.target} is set to "circonus". # # The default is # # metrics.circonus.apiurl = # metrics.circonus.brokerid configures a specific broker to use when # creating a check for submitting metrics to Circonus. # This is optional when ${metrics.target} is set to "circonus". # Optional for public brokers, required for Inside brokers. # Only applicable if a check is being created. # # The default is # # metrics.circonus.brokerid = # metrics.circonus.checkid configures a specific check to use when # submitting metrics to Circonus. # This is optional when ${metrics.target} is set to "circonus". # An attempt will be made to search for a previously created check, # if no applicable check is found, one will be created. # # The default is # # metrics.circonus.checkid = # metrics.prometheus.subsystem configures the system name when reporting # metrics. This is basically appended to the prefix for metric names. # # The default is # # metrics.prometheus.subsystem = # metrics.prometheus.path configures the path to serve up metrics on any configured # proxy.addr's where proto=prometheus. # # The default is # # metrics.prometheus.path = /metrics # metrics.prometheus.buckets configures the time buckets for use with histograms, measured in seconds. # for instance, .005 is equivalent to 5ms. there is an implied "infinity" bucket tacked on at the end. # # The default is # # metrics.prometheus.buckets = .005,.01,.025,.05,.1,.25,.5,1,2.5,5,10 # runtime.gogc configures GOGC (the GC target percentage). # # Setting runtime.gogc is equivalent to setting the GOGC # environment variable which also takes precedence over # the value from the config file. # # NOTE - the default for fabio up to 1.5.14 was 800. This changed # to 100 in version 1.5.15 # # The default is # # runtime.gogc = 100 # runtime.gomaxprocs configures GOMAXPROCS. # # Setting runtime.gomaxprocs is equivalent to setting the GOMAXPROCS # environment variable which also takes precedence over # the value from the config file. # # If runtime.gomaxprocs < 0 then all CPU cores are used. # # The default is # # runtime.gomaxprocs = -1 # ui.access configures the access mode for the UI. # # ro: read-only access # rw: read-write access # # The default is # # ui.access = rw # ui.addr configures the address the UI is listening on. # The listener uses the same syntax as proxy.addr but # supports only a single listener. To enable HTTPS # configure a certificate source. You should use # a different certificate source than the one you # use for the external connections, e.g. 'cs=ui'. # # The default is # # ui.addr = :9998 # ui.color configures the background color of the UI. # Color names are from http://materializecss.com/color.html # # The default is # # ui.color = light-green # ui.title configures an optional title for the UI. # # The default is # # ui.title = # ui.routingtable.source.linkenabled optionally configure if the # routing table's column "source" should be a clickable link. # # The default is # # ui.routingtable.source.linkenabled = false # ui.routingtable.source.newtab configures if the source # link should open in a new tab. # This is only applicable if the 'linkenabled' is set to true. # # The default is # # ui.routingtable.source.newtab = true # 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. # This is only applicable if the 'linkenabled' is set to true. # # The default is # # ui.routingtable.source.scheme = http # ui.routingtable.source.host configures an optional host or # base address for the link in the source column. # This 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. If this is not set, and the source link is # enabled, the link will default to current host. # This is only applicable if the 'linkenabled' is set to true. # # The default is # # ui.routingtable.source.host = # ui.routingtable.source.port configures an optional port # for the routing table source column link. This # is used in conjunction with the host and scheme. If the # source is not a separate server (does not begin with '/', # e.g. 'dev.google.net'), and the host is set, or default to # the current scheme protocol port (80 for http or 443 for https). # This is only applicable if the 'linkenabled' is set to true. # # The default is # # ui.routingtable.source.port = # Open Trace Configuration Currently supports ZipKin Collector # tracing.TracingEnabled enables/disables Open Tracing in Fabio. Bool value true/false # # The default is # # tracing.TracingEnabled = false # tracing.CollectorType sets what type of collector is used. # Currently only two types are supported http and kafka # # http: sets collector type to http tracing.ConnectString must also be set # kafka: sets collector type to emit via kafka. tracing.Topic must also be set # # The default is # # tracing.CollectorType = http # tracing.ConnectString sets the connection string per connection type. # If tracing.CollectorType = http tracing.ConnectString should be # http://URL:PORT where URL is the URL of your collector and PORT is the TCP Port # it is listening on # # If tracing.CollectorType = kafka tracing.ConnectString should be # HOSTNAME:PORT of your kafka broker # tracing.Topic must also be set # # The default is # # tracing.ConnectString = http://localhost:9411/api/v1/spans # tracing.ServiceName sets the service name used in reporting span information # # The default is # # tracing.ServiceName = Fabiolb # tracing.SpanName configures the template used in reporting span information # # The value is expanded by the text/template package and provides # the following variables: # # - Proto: the protocol version # - Method: the HTTP method # - Host: the host part of the URL # - Scheme: the scheme of the requested URL # - Path: the path of the requested URL # - RawQuery: the encoded query values of the requested URL # # SpanName defaults to the value of tracing.ServiceName but can be # overridden with this property. # # Example: tracing.SpanName = {{.Proto}} {{.Method}} {{.Path}} # # The default is # # tracing.SpanName = # tracing.Topic sets the Topic String used if tracing.CollectorType is kafka and # tracing.ConnectSting is set to a kafka broker # # The default is # # tracing.Topic = Fabiolb-Kafka-Topic # tracing.SamplerRate is the rate at which opentrace span data will be collected and sent # If SamplerRate is <= 0 Never sample # If SamplerRate is >= 1.0 always sample # Values between 0 and 1 will be the percentage in decimal form # Example a value of .50 will be 50% sample rate # # The default is # tracing.SamplerRate = -1 # tracing.SpanHost sets host information. # This is used to specify additional information when sending spans to a collector # # The default is # tracing.SpanHost = localhost:9998 # BGP Anycast configuration # Experimental. Leopards will eat your face. # bgp.enabled enables the embedded gobgpd daemon. # The default is # bgp.enabled = false # bgp.asn sets the asn ID of our router # The default is: # bgp.asn = 65000 # bgp.anycastaddresses sets the anycast addresses we will advertise, separated by comma. Technically this # will advertise any route prefix. These should already be configured on the host probably hung off loopback. # for example, 192.168.5.3/32. The default value is: # bgp.anycastaddresses = # # If bgp is enabled, this must be defined. # bgp.routerid is the router id (ip address) of this router. This is required if bgp is enabled. # the default value is: # bgp.routerid = # # bgp.listenport sets the listen ports for bgp communication from other routers. # default vaule is : # bgp.listenport = 179 # bgp.listenaddresses sets the listen addresses for bgp, separated by comma. The default is # bgp.listenaddresses = 0.0.0.0 # which listens on all interfaces. # bgp.nexthop sets the next hop address. If not set, it uses the bgp.routerid instead. # default value: # bgp.nexthop = # bgp.peers sets the bgp peers we will advertise routes to. This is required if bgp is enabled. # bgp.peers is specified as a comma separated list of neighboraddress and asn pairs, i.e. # bgp.peers = address=1.2.3.4;asn=65001,address=5.6.7.8;asn=65002 # valid parameters for peers are: # address - required # port - optional, defaults to 179 # asn - required # multihop - optional, defaults to false # multihoplength - optional, defaults to 2 # password - optional # default value # bgp.peers = # bgp.enablegrpc enables the gobgp grpc interface. To be used with the gobgp command line client. # default value is: # bgp.enablegrpc=false # bgp.grpclistenaddress is the listen interface and port if bgp.enablegrpc is set to true. defaults to: # bgp.grpclistenaddress = 127.0.0.1:50051 # bgp.grpctls is whether to enable TLS on the bgp grpc interface. default value is: # bgp.grpctls = false # bgp.certfile is the file path of the certificate, and is required if bgp.grpctls is set to true. Default value is: # bgp.certfile = # bgp.keyfile is the file path of the key file, and is required if bgp.grpctls is set to true. Default value is: # bgp.keyfile = # bgp.nexthop explicitly sets the value of the nexthop for all routes we publish. If not set, this uses the # bgp.routerid value, which is what makes sense in most cases. Default value is: # bgp.nexthop = # bgp.gobgpdcfgfile is the optional file path to a gobgpd config file. This overrides the global config # items above, such as bgp.routerid, bgp.asn etc. This also skips # # automatically adding gobgpd policies that prevent us from accepting prefixes from neighbors. only # use this if you know what you're doing, this is to allow for more flexibility than we expose directly # with fabio. # default value is: # bgp.gobgpdcfgfile = ================================================ FILE: go.mod ================================================ module github.com/fabiolb/fabio go 1.25.5 require ( github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f github.com/circonus-labs/circonus-gometrics/v3 v3.4.7 github.com/go-kit/kit v0.13.0 github.com/go-kit/log v0.2.1 github.com/gobwas/glob v0.2.3 github.com/hashicorp/consul/api v1.33.0 github.com/hashicorp/go-sockaddr v1.0.7 github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/sdk v0.20.0 github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252 github.com/magiconair/properties v1.8.10 github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 github.com/osrg/gobgp/v3 v3.37.0 github.com/pascaldekloe/goe v0.1.1 github.com/pkg/profile v1.7.0 github.com/prometheus/client_golang v1.23.2 github.com/rogpeppe/fastuuid v1.2.0 github.com/sergi/go-diff v1.4.0 github.com/tg123/go-htpasswd v1.2.4 golang.org/x/net v0.48.0 golang.org/x/sync v0.19.0 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 ) require ( github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/circonus-labs/go-apiclient v0.7.24 // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/eapache/channels v1.1.0 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/serf v0.10.2 // indirect github.com/k-sone/critbitgo v1.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openhistogram/circonusllhist v0.4.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f h1:SaJ6yqg936TshyeFZqQE+N+9hYkIeL9AMr7S4voCl10= github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonus-gometrics/v3 v3.4.7 h1:r7YBLIgiTT5Q4yNKSouP68M5mYT4djIQCng0dJdidiA= github.com/circonus-labs/circonus-gometrics/v3 v3.4.7/go.mod h1:57gznrTyBxQCsGYC/+3BN9POyrkEm4F3KsBTVQ5EQJk= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/circonus-labs/go-apiclient v0.7.15/go.mod h1:RFgkvdYEkimzgu3V2vVYlS1bitjOz1SF6uw109ieNeY= github.com/circonus-labs/go-apiclient v0.7.24 h1:ouJ/Dd/mlKOpG2ZRkuAvBBCn/YRQq4762MOnwIGdYQ8= github.com/circonus-labs/go-apiclient v0.7.24/go.mod h1:M284FyvP8iLy5SPxLxy5yrOxEjK8RSgRPKhcc6WFDA4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k= github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16 h1:ptucaU8cwiAc+/jqDblz0kb1ECLqPTeX/qQym8OBYzY= github.com/google/pprof v0.0.0-20251208000136-3d256cb9ff16/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/consul/api v1.33.0 h1:MnFUzN1Bo6YDGi/EsRLbVNgA4pyCymmcswrE5j4OHBM= github.com/hashicorp/consul/api v1.33.0/go.mod h1:vLz2I/bqqCYiG0qRHGerComvbwSWKswc8rRFtnYBrIw= github.com/hashicorp/consul/sdk v0.17.0 h1:N/JigV6y1yEMfTIhXoW0DXUecM2grQnFuRpY7PcLHLI= github.com/hashicorp/consul/sdk v0.17.0/go.mod h1:8dgIhY6VlPUprRH7o7UenVuFEgq017qUn3k9wS5mCt4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI= github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/hashicorp/vault/sdk v0.20.0 h1:a4ulj2gICzw/qH0A4+6o36qAHxkUdcmgpMaSSjqE3dc= github.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252 h1:jeqlfkFa5h+Ak/I33QpU4p01nFhw0G5IFm/Rsenne2Y= github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252/go.mod h1:R6mExYS3O0XXjOZye3GtXfbuGF4hWQnF45CFWoj7O6g= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrbE= github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 h1:62uLwA3l2JMH84liO4ZhnjTH5PjFyCYxbHLgXPaJMtI= github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9/go.mod h1:MvMXoufZAtqExNexqi4cjrNYE9MefKddKylxjS+//n0= github.com/openhistogram/circonusllhist v0.3.0/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0= github.com/openhistogram/circonusllhist v0.4.1 h1:oolOK2dTxFPIu9epYukSgINQ5no58iAWZfQ5SZt23vk= github.com/openhistogram/circonusllhist v0.4.1/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/osrg/gobgp/v3 v3.37.0 h1:+ObuOdvj7G7nxrT0fKFta+EAupdWf/q1WzbXydr8IOY= github.com/osrg/gobgp/v3 v3.37.0/go.mod h1:kVHVFy1/fyZHJ8P32+ctvPeJogn9qKwa1YCeMRXXrP0= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.1 h1:Ah6WQ56rZONR3RW3qWa2NCZ6JAVvSpUcoLBaOmYFt9Q= github.com/pascaldekloe/goe v0.1.1/go.mod h1:KSyfaxQOh0HZPjDP1FL/kFtbqYqrALJTaMafFUIccqU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU= github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= ================================================ FILE: logger/level_writer.go ================================================ package logger import ( "fmt" "io" "strings" "sync/atomic" ) // LevelWriter implements a simplistic levelled log writer which supports // TRACE, DEBUG, INFO, WARN, ERROR and FATAL. The log level can be changed at // runtime. type LevelWriter struct { w io.Writer level atomic.Value // string prefixLen int } // NewLevelWriter creates a new leveled writer for the given output and a // default level. Prefix is the string that is expected before the opening // bracket and usually depends on the chosen log format. For the default log // format prefix should be set to "2017/01/01 00:00:00 " whereby only the // format and the spaces are relevant but not the date and time itself. func NewLevelWriter(w io.Writer, level, prefix string) *LevelWriter { lw := &LevelWriter{w: w, prefixLen: len(prefix)} if !lw.SetLevel(level) { panic(fmt.Sprintf("invalid log level %s", level)) } return lw } func (w *LevelWriter) Write(b []byte) (int, error) { // check if the log line starts with the prefix if len(b) < w.prefixLen+2 || b[w.prefixLen] != '[' { return fmt.Fprint(w.w, "invalid log msg: ", string(b)) } // determine the level by looking at the character after the opening // bracket. level := rune(b[w.prefixLen+1]) // T, D, I, W, E, or F // w.level contains the characters of all the allowed levels so we can just // check whether the level character is in that set. if strings.ContainsRune(w.level.Load().(string), level) { return w.w.Write(b) } return 0, nil } // SetLevel sets the log level to the new value and returns true // if that was successful. func (w *LevelWriter) SetLevel(s string) bool { // levels contains the first character of the levels in descending order const levels = "TDIWEF" switch strings.ToUpper(s) { case "TRACE": w.level.Store(levels[0:]) return true case "DEBUG": w.level.Store(levels[1:]) return true case "INFO": w.level.Store(levels[2:]) return true case "WARN": w.level.Store(levels[3:]) return true case "ERROR": w.level.Store(levels[4:]) return true case "FATAL": w.level.Store(levels[5:]) return true default: return false } } // Level returns the current log level. func (w *LevelWriter) Level() string { l := w.level.Load().(string) switch l[0] { case 'T': return "TRACE" case 'D': return "DEBUG" case 'I': return "INFO" case 'W': return "WARN" case 'E': return "ERROR" case 'F': return "FATAL" default: return "???" + l + "???" } } ================================================ FILE: logger/level_writer_test.go ================================================ package logger import ( "bytes" "reflect" "strings" "testing" ) func TestLevelWriter(t *testing.T) { input := []string{ "2017/01/01 00:00:00 [TRACE] a", "2017/01/01 00:00:00 [DEBUG] a", "2017/01/01 00:00:00 [INFO] a", "2017/01/01 00:00:00 [WARN] a", "2017/01/01 00:00:00 [ERROR] a", "2017/01/01 00:00:00 [FATAL] a", } tests := []struct { level string out []string }{ {"TRACE", input}, {"DEBUG", input[1:]}, {"INFO", input[2:]}, {"WARN", input[3:]}, {"ERROR", input[4:]}, {"FATAL", input[5:]}, } for _, tt := range tests { t.Run(tt.level, func(t *testing.T) { var b bytes.Buffer w := NewLevelWriter(&b, tt.level, "2017/01/01 00:00:00 ") for _, s := range input { if _, err := w.Write([]byte(s + "\n")); err != nil { t.Fatal("w.Write:", err) } } out := strings.Split(strings.TrimRight(b.String(), "\n"), "\n") if got, want := out, tt.out; !reflect.DeepEqual(got, want) { t.Fatalf("got %#v want %#v", got, want) } }) } } ================================================ FILE: logger/logger.go ================================================ // Package logger implements a configurable access logger. // // The access log format is defined through a format string which expands to a // log line per request. The values are taken as is and no quoting or escaping // takes place. Text between two fields is printed verbatim. See the common // log file formats for an example. // // $header. - request http header (name: [a-zA-Z0-9-]+) // $remote_addr - host:port of remote client // $remote_host - host of remote client // $remote_port - port of remote client // $request - request // $request_args - request query parameters // $request_host - request host header (aka server name) // $request_method - request method // $request_scheme - request scheme // $request_uri - request URI // $request_url - request URL // $request_proto - request protocol // $response_body_size - response body size in bytes // $response_status - response status code // $response_time_ms - response time in S.sss format // $response_time_us - response time in S.ssssss format // $response_time_ns - response time in S.sssssssss format // $time_rfc3339 - log timestamp in YYYY-MM-DDTHH:MM:SSZ format // $time_rfc3339_ms - log timestamp in YYYY-MM-DDTHH:MM:SS.sssZ format // $time_rfc3339_us - log timestamp in YYYY-MM-DDTHH:MM:SS.ssssssZ format // $time_rfc3339_ns - log timestamp in YYYY-MM-DDTHH:MM:SS.sssssssssZ format // $time_unix_ms - log timestamp in unix epoch ms // $time_unix_us - log timestamp in unix epoch us // $time_unix_ns - log timestamp in unix epoch ns // $time_common - log timestamp in DD/MMM/YYYY:HH:MM:SS -ZZZZ // $upstream_addr - host:port of upstream server // $upstream_host - host of upstream server // $upstream_port - port of upstream server // $upstream_request_scheme - upstream request scheme // $upstream_request_uri - upstream request URI // $upstream_request_url - upstream request URL package logger import ( "bytes" "errors" "io" "net/http" "net/url" "sync" "time" ) // Common log file formats. const ( CommonFormat = `$remote_host - - [$time_common] "$request" $response_status $response_body_size` CombinedFormat = `$remote_host - - [$time_common] "$request" $response_status $response_body_size "$header.Referer" "$header.User-Agent"` ) // Event defines the elements of a loggable event. type Event struct { // Start is the time when the action that triggered the event started. Start time.Time // End is the time when the action that triggered the event was completed. End time.Time // Request is the HTTP request that is connected to this event. // It should only be set for HTTP log events. Request *http.Request // Response is the HTTP response which is connected to this event. // It should only be set for HTTP log events. Response *http.Response // RequestURL is the URL of the incoming HTTP request. // It should only be set for HTTP log events. RequestURL *url.URL // UpstreamURL is the URL which was sent to the upstream server. // It should only be set for HTTP log events. UpstreamURL *url.URL // UpstreamAddr is the TCP address in the form of "host:port" of the // upstream server which handled the proxied request. UpstreamAddr string // UpstreamService is the name of the upstream service as // defined in the route. UpstreamService string } // Logger logs an event. type Logger interface { Log(event *Event) } // New creates a new logger that writes log events in the given format to the // provided writer. If no writer was provided no log output is generated. // If the format is empty or invalid an error is returned. func New(w io.Writer, format string) (Logger, error) { if w == nil { return &noopLogger{}, nil } p, err := parse(format, fields) if err != nil { return nil, err } if len(p) == 0 { return nil, errors.New("empty log format") } return &logger{p: p, w: w}, nil } type noopLogger struct{} func (l *noopLogger) Log(*Event) {} type logger struct { w io.Writer p pattern mu sync.Mutex } // bufSize defines the default size of the log buffers. const bufSize = 1024 // pool provides a reusable set of log buffers. var pool = sync.Pool{ New: func() interface{} { return bytes.NewBuffer(make([]byte, 0, bufSize)) }, } // Log writes a log line for the request that was executed // between t1 and t2. func (l *logger) Log(e *Event) { b := pool.Get().(*bytes.Buffer) b.Reset() l.p.write(b, e) l.mu.Lock() l.w.Write(b.Bytes()) l.mu.Unlock() pool.Put(b) } ================================================ FILE: logger/logger_test.go ================================================ package logger import ( "bytes" "io" "net/http" "net/url" "sort" "strings" "testing" "text/template" "time" ) func TestParse(t *testing.T) { fields := map[string]field{ "$a": func(b *bytes.Buffer, e *Event) { b.WriteString("aa") }, "$b": func(b *bytes.Buffer, e *Event) { b.WriteString("bb") }, } req := &http.Request{ Header: http.Header{ "User-Agent": {"Mozilla Firefox"}, "X-Forwarded-For": {"3.3.3.3"}, }, } tests := []struct { format string out string }{ {"", ""}, {"$a", "aa\n"}, {"$a $b", "aa bb\n"}, {"$a \"$header.User-Agent\"", "aa \"Mozilla Firefox\"\n"}, } for i, tt := range tests { p, err := parse(tt.format, fields) if err != nil { t.Errorf("%d: got %v want nil", i, err) continue } var b bytes.Buffer p.write(&b, &Event{Start: time.Time{}, End: time.Time{}, Request: req}) if got, want := b.String(), tt.out; got != want { t.Errorf("%d: got %q want %q", i, got, want) } } } func TestLog(t *testing.T) { rurl := mustParse("http://foo.com/?q=x") uurl := mustParse("http://7.8.9.0:5678/foo?q=x") start := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC) e := &Event{ Start: start, End: start.Add(123456789 * time.Nanosecond), Request: &http.Request{ RequestURI: rurl.RequestURI(), Header: http.Header{ "User-Agent": {"Mozilla Firefox"}, "Referer": {"http://foo.com/"}, "X-Forwarded-For": {"3.3.3.3"}, }, RemoteAddr: "2.2.2.2:666", Host: rurl.Host, URL: rurl, Method: "GET", Proto: "HTTP/1.1", }, Response: &http.Response{ StatusCode: 200, ContentLength: 1234, Header: http.Header{"foo": []string{"bar"}}, Request: &http.Request{ RemoteAddr: "5.6.7.8:1234", }, }, RequestURL: rurl, UpstreamAddr: uurl.Host, UpstreamService: "svc-a", UpstreamURL: uurl, } tests := []struct { format string out string }{ {"$header.Referer", "http://foo.com/\n"}, {"$header.X-Forwarded-For", "3.3.3.3\n"}, {"$header.user-agent", "Mozilla Firefox\n"}, {"$remote_addr", "2.2.2.2:666\n"}, {"$remote_host", "2.2.2.2\n"}, {"$remote_port", "666\n"}, {"$request", "GET /?q=x HTTP/1.1\n"}, {"$request_args", "q=x\n"}, {"$request_host", "foo.com\n"}, // TODO(fs): is this correct? {"$request_method", "GET\n"}, {"$request_proto", "HTTP/1.1\n"}, {"$request_scheme", "http\n"}, {"$request_uri", "/?q=x\n"}, {"$request_url", "http://foo.com/?q=x\n"}, {"$response_body_size", "1234\n"}, {"$response_status", "200\n"}, {"$response_time_ms", "0.123\n"}, // TODO(fs): is this correct? {"$response_time_ns", "0.123456789\n"}, // TODO(fs): is this correct? {"$response_time_us", "0.123456\n"}, // TODO(fs): is this correct? {"$time_common", "01/Jan/2016:00:00:00 +0000\n"}, {"$time_rfc3339", "2016-01-01T00:00:00Z\n"}, {"$time_rfc3339_ms", "2016-01-01T00:00:00.123Z\n"}, {"$time_rfc3339_ns", "2016-01-01T00:00:00.123456789Z\n"}, {"$time_rfc3339_us", "2016-01-01T00:00:00.123456Z\n"}, {"$time_unix_ms", "1451606400123\n"}, {"$time_unix_ns", "1451606400123456789\n"}, {"$time_unix_us", "1451606400123456\n"}, {"$upstream_addr", "7.8.9.0:5678\n"}, {"$upstream_host", "7.8.9.0\n"}, {"$upstream_port", "5678\n"}, {"$upstream_request_scheme", "http\n"}, {"$upstream_request_uri", "/foo?q=x\n"}, {"$upstream_request_url", "http://7.8.9.0:5678/foo?q=x\n"}, {"$upstream_service", "svc-a\n"}, } for _, tt := range tests { t.Run(tt.format, func(t *testing.T) { b := new(bytes.Buffer) l, err := New(b, tt.format) if err != nil { t.Fatalf("got %v want nil", err) } l.Log(e) if got, want := b.String(), tt.out; got != want { t.Errorf("got %q want %q", got, want) } }) } } func TestAtoi(t *testing.T) { tests := []struct { i int64 pad int s string }{ {i: 0, pad: 0, s: "0"}, {i: 1, pad: 0, s: "1"}, {i: -1, pad: 0, s: "-1"}, {i: 12345, pad: 0, s: "12345"}, {i: -12345, pad: 0, s: "-12345"}, {i: 9223372036854775807, pad: 0, s: "9223372036854775807"}, {i: -9223372036854775807, pad: 0, s: "-9223372036854775807"}, {i: 0, pad: 5, s: "00000"}, {i: 1, pad: 5, s: "00001"}, {i: -1, pad: 5, s: "-00001"}, {i: 12345, pad: 5, s: "12345"}, {i: -12345, pad: 5, s: "-12345"}, {i: 9223372036854775807, pad: 5, s: "9223372036854775807"}, {i: -9223372036854775807, pad: 5, s: "-9223372036854775807"}, } for i, tt := range tests { var b bytes.Buffer atoi(&b, tt.i, tt.pad) if got, want := b.String(), tt.s; got != want { t.Errorf("%d: got %q want %q", i, got, want) } } } func BenchmarkLog(b *testing.B) { start := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC) e := &Event{ Start: start, End: start.Add(100 * time.Millisecond), Request: &http.Request{ RequestURI: "/?q=x", Header: http.Header{ "User-Agent": {"Mozilla Firefox"}, "Referer": {"http://foo.com/"}, "X-Forwarded-For": {"3.3.3.3"}, }, RemoteAddr: "2.2.2.2:666", Host: "foo.com", URL: &url.URL{ Path: "/", RawQuery: "?q=x", Host: "proxy host", }, Method: "GET", Proto: "HTTP/1.1", }, Response: &http.Response{ StatusCode: 200, ContentLength: 1234, Header: http.Header{"foo": []string{"bar"}}, Request: &http.Request{ RemoteAddr: "5.6.7.8:1234", }, }, UpstreamAddr: mustParse("http://7.8.9.0:5678/foo").Host, } // benchmark the custom parser and text/template // to explain why there is a custom approach. // The custom parser is 8x faster and has zero allocs. // // BenchmarkLog/my_parser-8 1000000 2326 ns/op 0 B/op 0 allocs/op // BenchmarkLog/go_text/template-8 100000 19026 ns/op 848 B/op 76 allocs/op b.Run("custom parser", func(b *testing.B) { var keys []string for k := range fields { keys = append(keys, k) } sort.Strings(keys) format := strings.Join(keys, " ") l, err := New(io.Discard, format) if err != nil { b.Fatal(err) } b.ResetTimer() for range b.N { l.Log(e) } }) b.Run("text/template", func(b *testing.B) { // simulate the text template approach by using // the same number of fields as for the other parser // but using the same value. tmpl := "" for range fields { tmpl += "{{.Req.RemoteAddr}}" } t := template.Must(template.New("log").Parse(tmpl)) b.ResetTimer() for range b.N { t.Execute(io.Discard, e) } }) } func mustParse(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(err) } return u } ================================================ FILE: logger/pattern.go ================================================ package logger import ( "bytes" "fmt" "sort" "strings" "time" ) func init() { for f := range fields { Fields = append(Fields, f) } sort.Strings(Fields) } // Fields contains a list of all known static log fields in alphabetical order. var Fields []string // pattern is a log output format. type pattern []field func (p pattern) write(b *bytes.Buffer, e *Event) { for _, fn := range p { fn(b, e) } if b.Len() == 0 { return } b.WriteRune('\n') } // field renders a part of the log line. type field func(b *bytes.Buffer, e *Event) // fields contains the known log fields and their field functions. The field // functions should avoid to alloc memory at all cost since they are in the hot // path. Do not use fmt.Sprintf() but combine the value from the parts. Instead // of strconv.Atoi/FormatInt() use the local atoi() function which does not // alloc. var fields = map[string]field{ "$remote_addr": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.RemoteAddr) }, "$remote_host": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } host, _ := hostport(e.Request.RemoteAddr) b.WriteString(host) }, "$remote_port": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } _, port := hostport(e.Request.RemoteAddr) b.WriteString(port) }, "$request": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.Method) b.WriteRune(' ') b.WriteString(e.Request.RequestURI) b.WriteRune(' ') b.WriteString(e.Request.Proto) }, "$request_args": func(b *bytes.Buffer, e *Event) { // cannot use e.Req.URL since it may have been modified if e.RequestURL == nil { return } b.WriteString(e.RequestURL.RawQuery) }, "$request_host": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.Host) }, "$request_method": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.Method) }, "$request_scheme": func(b *bytes.Buffer, e *Event) { // cannot use e.Req.URL since it may have been modified if e.RequestURL == nil { return } b.WriteString(e.RequestURL.Scheme) }, "$request_uri": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.RequestURI) }, "$request_url": func(b *bytes.Buffer, e *Event) { // cannot use e.Req.URL since it may have been modified if e.RequestURL == nil { return } b.WriteString(e.RequestURL.String()) }, "$request_proto": func(b *bytes.Buffer, e *Event) { if e.Request == nil { return } b.WriteString(e.Request.Proto) }, "$response_body_size": func(b *bytes.Buffer, e *Event) { atoi(b, e.Response.ContentLength, 0) }, "$response_status": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.Response.StatusCode), 0) }, "$response_time_ms": func(b *bytes.Buffer, e *Event) { d := e.End.Sub(e.Start).Nanoseconds() s, us := d/int64(time.Second), d%int64(time.Second)/int64(time.Millisecond) atoi(b, s, 0) b.WriteRune('.') atoi(b, us, 3) }, "$response_time_us": func(b *bytes.Buffer, e *Event) { d := e.End.Sub(e.Start).Nanoseconds() s, us := d/int64(time.Second), d%int64(time.Second)/int64(time.Microsecond) atoi(b, s, 0) b.WriteRune('.') atoi(b, us, 6) }, "$response_time_ns": func(b *bytes.Buffer, e *Event) { d := e.End.Sub(e.Start).Nanoseconds() s, ns := d/int64(time.Second), d%int64(time.Second)/int64(time.Nanosecond) atoi(b, s, 0) b.WriteRune('.') atoi(b, ns, 9) }, "$time_unix_ms": func(b *bytes.Buffer, e *Event) { atoi(b, e.End.UnixNano()/int64(time.Millisecond), 0) }, "$time_unix_us": func(b *bytes.Buffer, e *Event) { atoi(b, e.End.UnixNano()/int64(time.Microsecond), 0) }, "$time_unix_ns": func(b *bytes.Buffer, e *Event) { atoi(b, e.End.UnixNano(), 0) }, "$time_common": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.End.Day()), 2) b.WriteRune('/') b.WriteString(shortMonthNames[e.End.Month()]) b.WriteRune('/') atoi(b, int64(e.End.Year()), 4) b.WriteRune(':') atoi(b, int64(e.End.Hour()), 2) b.WriteRune(':') atoi(b, int64(e.End.Minute()), 2) b.WriteRune(':') atoi(b, int64(e.End.Second()), 2) b.WriteString(" +0000") // TODO(fs): local time }, "$time_rfc3339": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.End.Year()), 4) b.WriteRune('-') atoi(b, int64(e.End.Month()), 2) b.WriteRune('-') atoi(b, int64(e.End.Day()), 2) b.WriteRune('T') atoi(b, int64(e.End.Hour()), 2) b.WriteRune(':') atoi(b, int64(e.End.Minute()), 2) b.WriteRune(':') atoi(b, int64(e.End.Second()), 2) b.WriteRune('Z') }, "$time_rfc3339_ms": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.End.Year()), 4) b.WriteRune('-') atoi(b, int64(e.End.Month()), 2) b.WriteRune('-') atoi(b, int64(e.End.Day()), 2) b.WriteRune('T') atoi(b, int64(e.End.Hour()), 2) b.WriteRune(':') atoi(b, int64(e.End.Minute()), 2) b.WriteRune(':') atoi(b, int64(e.End.Second()), 2) b.WriteRune('.') atoi(b, int64(e.End.Nanosecond())/int64(time.Millisecond), 3) b.WriteRune('Z') }, "$time_rfc3339_us": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.End.Year()), 4) b.WriteRune('-') atoi(b, int64(e.End.Month()), 2) b.WriteRune('-') atoi(b, int64(e.End.Day()), 2) b.WriteRune('T') atoi(b, int64(e.End.Hour()), 2) b.WriteRune(':') atoi(b, int64(e.End.Minute()), 2) b.WriteRune(':') atoi(b, int64(e.End.Second()), 2) b.WriteRune('.') atoi(b, int64(e.End.Nanosecond())/int64(time.Microsecond), 6) b.WriteRune('Z') }, "$time_rfc3339_ns": func(b *bytes.Buffer, e *Event) { atoi(b, int64(e.End.Year()), 4) b.WriteRune('-') atoi(b, int64(e.End.Month()), 2) b.WriteRune('-') atoi(b, int64(e.End.Day()), 2) b.WriteRune('T') atoi(b, int64(e.End.Hour()), 2) b.WriteRune(':') atoi(b, int64(e.End.Minute()), 2) b.WriteRune(':') atoi(b, int64(e.End.Second()), 2) b.WriteRune('.') atoi(b, int64(e.End.Nanosecond()), 9) b.WriteRune('Z') }, "$upstream_addr": func(b *bytes.Buffer, e *Event) { b.WriteString(e.UpstreamAddr) }, "$upstream_host": func(b *bytes.Buffer, e *Event) { host, _ := hostport(e.UpstreamAddr) b.WriteString(host) }, "$upstream_port": func(b *bytes.Buffer, e *Event) { _, port := hostport(e.UpstreamAddr) b.WriteString(port) }, "$upstream_request_scheme": func(b *bytes.Buffer, e *Event) { if e.UpstreamURL == nil { return } b.WriteString(e.UpstreamURL.Scheme) }, "$upstream_request_uri": func(b *bytes.Buffer, e *Event) { if e.UpstreamURL == nil { return } b.WriteString(e.UpstreamURL.RequestURI()) }, "$upstream_request_url": func(b *bytes.Buffer, e *Event) { if e.UpstreamURL == nil { return } b.WriteString(e.UpstreamURL.String()) }, "$upstream_service": func(b *bytes.Buffer, e *Event) { b.WriteString(e.UpstreamService) }, } var shortMonthNames = []string{ "---", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", } // hostport is a simplified no-alloc version of // net.SplitHostPort. Since we know that the // address values have the correct form we can // skip all the error checking. func hostport(s string) (host, port string) { if s == "" { return "", "" } n := strings.LastIndexByte(s, ':') return s[:n], s[n+1:] } // atoi is a replacement for strconv.Atoi/strconv.FormatInt // which does not alloc. func atoi(b *bytes.Buffer, i int64, pad int) { var flag bool if i < 0 { flag = true i = -i } // format number // 2^63-1 == 9223372036854775807 var d [128]byte n, p := len(d), len(d)-1 for i >= 0 { d[p] = byte('0') + byte(i%10) i /= 10 p-- if i == 0 { break } } // padding for n-p-1 < pad { d[p] = byte('0') p-- } if flag { d[p] = '-' p-- } b.Write(d[p+1:]) } // parse parses a format string into a pattern based on the following rules: // // The format string consists of text and fields. Field names start with a '$' // and consist of ASCII characters [a-zA-Z0-9.-_]. Field names like // '$header.name' will render the HTTP header 'name'. All other field names // must exist in the fields map. func parse(format string, fields map[string]field) (p pattern, err error) { // text is a helper to add raw text to the log output. text := func(s string) field { return func(b *bytes.Buffer, e *Event) { b.WriteString(s) } } // header is a helper to add an HTTP header to the log output. header := func(name string) field { return func(b *bytes.Buffer, e *Event) { if e.Request == nil || e.Request.Header == nil { return } b.WriteString(e.Request.Header.Get(name)) } } s := []rune(format) for len(s) > 0 { typ, n := lex(s) val := string(s[:n]) s = s[n:] switch typ { case itemText: p = append(p, text(val)) case itemHeader: p = append(p, header(val[len("$header."):])) case itemField: f := fields[val] if f == nil { return nil, fmt.Errorf("invalid field %q", val) } p = append(p, f) } } return p, nil } type itemType int const ( itemText itemType = iota itemField itemHeader ) func (t itemType) String() string { switch t { case itemText: return "TEXT" case itemField: return "FIELD" case itemHeader: return "HEADER" } panic("invalid") } type state int const ( stateStart state = iota stateText stateDollar stateField stateDot stateHeader ) func lex(s []rune) (typ itemType, n int) { isIDChar := func(r rune) bool { return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || '0' <= r && r <= '9' || r == '_' || r == '-' } state := stateStart for i, r := range s { switch state { case stateStart: switch r { case '$': state = stateDollar default: state = stateText } case stateText: switch r { case '$': return itemText, i default: // state = stateText } case stateDollar: switch { case isIDChar(r): state = stateField default: state = stateText } case stateField: switch { case r == '.': if string(s[:i]) == "$header" { state = stateDot } else { return itemField, i } case isIDChar(r): // state = stateField default: return itemField, i } case stateDot: switch { case isIDChar(r): state = stateHeader default: return itemField, i } case stateHeader: switch { case isIDChar(r): // state = stateHeader default: return itemHeader, i } } } switch state { case stateDot: return itemField, len(s) - 1 case stateField: return itemField, len(s) case stateHeader: return itemHeader, len(s) default: return itemText, len(s) } } ================================================ FILE: main.go ================================================ package main import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "runtime" "runtime/debug" "strings" "sync" "sync/atomic" "time" "github.com/fabiolb/fabio/bgp" "github.com/fabiolb/fabio/transport" gkm "github.com/go-kit/kit/metrics" "github.com/fabiolb/fabio/admin" "github.com/fabiolb/fabio/auth" "github.com/fabiolb/fabio/cert" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/exit" "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/metrics" "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy" "github.com/fabiolb/fabio/proxy/tcp" "github.com/fabiolb/fabio/registry" "github.com/fabiolb/fabio/registry/consul" "github.com/fabiolb/fabio/registry/custom" "github.com/fabiolb/fabio/registry/file" "github.com/fabiolb/fabio/registry/static" "github.com/fabiolb/fabio/route" grpc_proxy "github.com/mwitkow/grpc-proxy/proxy" "github.com/pkg/profile" dmp "github.com/sergi/go-diff/diffmatchpatch" "google.golang.org/grpc" ) // version contains the version number // // It is set by build/release.sh for tagged releases // so that 'go get' just works. // // It is also set by the linker when fabio // is built via the Makefile or the build/docker.sh // script to ensure the correct version number var version = "1.6.11" var shuttingDown int32 func main() { logOutput := logger.NewLevelWriter(os.Stderr, "INFO", "2017/01/01 00:00:00 ") log.SetOutput(logOutput) cfg, err := config.Load(os.Args, os.Environ()) if err != nil { exit.Fatalf("[FATAL] %s. %s", version, err) } if cfg == nil { fmt.Printf("%s %s\n", version, runtime.Version()) return } transport.SetConfig(cfg) log.Printf("[INFO] Setting log level to %s", logOutput.Level()) if !logOutput.SetLevel(cfg.Log.Level) { log.Printf("[INFO] Cannot set log level to %s", cfg.Log.Level) } log.Printf("%s", "[INFO] Runtime config\n"+toJSON(cfg)) log.Printf("[INFO] Version %s starting", version) log.Printf("[INFO] Go runtime is %s", runtime.Version()) // warn once so that it is at the beginning of the log // this will also start the reminder go routine if necessary. WarnIfRunAsRoot(cfg.Insecure) // setup profiling if enabled var prof interface { Stop() } if cfg.ProfileMode != "" { var mode func(*profile.Profile) switch cfg.ProfileMode { case "": // do nothing case "cpu": mode = profile.CPUProfile case "mem": mode = profile.MemProfile case "mutex": mode = profile.MutexProfile case "block": mode = profile.BlockProfile case "trace": mode = profile.TraceProfile default: log.Fatalf("[FATAL] Invalid profile mode %q", cfg.ProfileMode) } prof = profile.Start(mode, profile.ProfilePath(cfg.ProfilePath), profile.NoShutdownHook) log.Printf("[INFO] Profile mode %q", cfg.ProfileMode) log.Printf("[INFO] Profile path %q", cfg.ProfilePath) } if cfg.BGP.BGPEnabled { err = bgp.ValidateConfig(&cfg.BGP) if err != nil { log.Fatalf("[FATAL] BGP configuration invalid: %s", err) } } exit.Listen(func(s os.Signal) { atomic.StoreInt32(&shuttingDown, 1) if registry.Default != nil { registry.Default.DeregisterAll() } time.Sleep(cfg.Proxy.DeregisterGracePeriod) proxy.Shutdown(cfg.Proxy.ShutdownWait) if prof != nil { prof.Stop() } }) metrics, err := metrics.Initialize(&cfg.Metrics) if err != nil { exit.Fatal("[FATAL] ", err) } route.SetMetricsProvider(metrics) initRuntime(cfg) initBackend(cfg) startAdmin(cfg) go watchNoRouteHTML() first := make(chan bool) go watchBackend(cfg, first) log.Print("[INFO] Waiting for first routing table") <-first // create proxies after metrics since they use the metrics registry. startServers(cfg, metrics) // warn again so that it is visible in the terminal WarnIfRunAsRoot(cfg.Insecure) if cfg.BGP.BGPEnabled { startBGP(&cfg.BGP) } exit.Wait() log.Print("[INFO] Down") } func newGrpcProxy(cfg *config.Config, tlscfg *tls.Config, statsHandler *proxy.GrpcStatsHandler) []grpc.ServerOption { //Init Glob Cache globCache := route.NewGlobCache(cfg.GlobCacheSize) proxyInterceptor := proxy.GrpcProxyInterceptor{ Config: cfg, StatsHandler: statsHandler, GlobCache: globCache, } handler := grpc_proxy.TransparentHandler(proxy.GetGRPCDirector(tlscfg, cfg)) return []grpc.ServerOption{ grpc.UnknownServiceHandler(handler), grpc.StreamInterceptor(proxyInterceptor.Stream), grpc.StatsHandler(statsHandler), grpc.MaxRecvMsgSize(cfg.Proxy.GRPCMaxRxMsgSize), grpc.MaxSendMsgSize(cfg.Proxy.GRPCMaxTxMsgSize), } } func newHTTPProxy(cfg *config.Config, statsHandler *proxy.HttpStatsHandler) *proxy.HTTPProxy { var w io.Writer //Init Glob Cache globCache := route.NewGlobCache(cfg.GlobCacheSize) switch cfg.Log.AccessTarget { case "": log.Printf("[INFO] Access logging disabled") case "stdout": log.Printf("[INFO] Writing access log to stdout") w = os.Stdout default: exit.Fatal("[FATAL] Invalid access log target ", cfg.Log.AccessTarget) } format := cfg.Log.AccessFormat switch format { case "common": format = logger.CommonFormat case "combined": format = logger.CombinedFormat } l, err := logger.New(w, format) if err != nil { exit.Fatal("[FATAL] Invalid log format: ", err) } pick := route.Picker[cfg.Proxy.Strategy] match := route.Matcher[cfg.Proxy.Matcher] log.Printf("[INFO] Using routing strategy %q", cfg.Proxy.Strategy) log.Printf("[INFO] Using route matching %q", cfg.Proxy.Matcher) authSchemes, err := auth.LoadAuthSchemes(cfg.Proxy.AuthSchemes) if err != nil { exit.Fatal("[FATAL] ", err) } return &proxy.HTTPProxy{ Config: cfg.Proxy, Transport: transport.NewTransport(nil), InsecureTransport: transport.NewTransport(&tls.Config{InsecureSkipVerify: true}), Lookup: func(r *http.Request) *route.Target { t := route.GetTable().Lookup(r, pick, match, globCache, cfg.GlobMatchingDisabled) if t == nil { statsHandler.Noroute.Add(1) log.Print("[WARN] No route for ", r.Host, r.URL) } return t }, Logger: l, AuthSchemes: authSchemes, Stats: *statsHandler, } } func lookupHostFn(cfg *config.Config, notFound gkm.Counter) func(string) *route.Target { pick := route.Picker[cfg.Proxy.Strategy] return func(host string) *route.Target { t := route.GetTable().LookupHost(host, pick) if t == nil { notFound.Add(1) log.Print("[WARN] No route for ", host) } return t } } // Returns a matcher function compatible with tcpproxy Matcher from github.com/inetaf/tcpproxy func lookupHostMatcher(cfg *config.Config) func(context.Context, string) bool { pick := route.Picker[cfg.Proxy.Strategy] return func(ctx context.Context, host string) bool { t := route.GetTable().LookupHost(host, pick) if t == nil { return false } // Make sure this is supposed to be a tcp proxy. // opts proto= overrides scheme if present. var ( ok bool proto string ) if proto, ok = t.Opts["proto"]; !ok && t.URL != nil { proto = t.URL.Scheme } return proto == "tcp" } } func makeTLSConfig(l config.Listen) (*tls.Config, error) { if l.CertSource.Name == "" { return nil, nil } src, err := cert.NewSource(l.CertSource) if err != nil { return nil, fmt.Errorf("failed to create cert source %s. %s", l.CertSource.Name, err) } tlscfg, err := cert.TLSConfig(src, l.StrictMatch, l.TLSMinVersion, l.TLSMaxVersion, l.TLSCiphers) if err != nil { return nil, fmt.Errorf("[FATAL] Failed to create TLS config for cert source %s. %s", l.CertSource.Name, err) } return tlscfg, nil } func startAdmin(cfg *config.Config) { log.Printf("[INFO] Admin server access mode %q", cfg.UI.Access) log.Printf("[INFO] Admin server listening on %q", cfg.UI.Listen.Addr) go func() { l := cfg.UI.Listen tlscfg, err := makeTLSConfig(l) if err != nil { exit.Fatal("[FATAL] ", err) } srv := &admin.Server{ Access: cfg.UI.Access, Color: cfg.UI.Color, Title: cfg.UI.Title, Version: version, Commands: route.Commands, Cfg: cfg, } if err := srv.ListenAndServe(l, tlscfg); err != nil { exit.Fatal("[FATAL] ui: ", err) } }() } func startServers(cfg *config.Config, stats metrics.Provider) { notFound := stats.NewCounter("notfound") var ( tcpConn gkm.Counter tcpConnFail gkm.Counter tcpNoRoute gkm.Counter tcpSniConn gkm.Counter tcpSniConnFail gkm.Counter tcpSniNoRoute gkm.Counter grpStatsHandler *proxy.GrpcStatsHandler httpStatsHandler *proxy.HttpStatsHandler ) grpcCounters := func() { grpStatsHandler = &proxy.GrpcStatsHandler{ Connect: stats.NewCounter("grpc.conn"), Request: stats.NewHistogram("grpc.requests"), NoRoute: stats.NewCounter("grpc.noroute"), Status: stats.NewHistogram("grep.status", "code"), } } var grpcOnce sync.Once httpCounters := func() { httpStatsHandler = &proxy.HttpStatsHandler{ Requests: stats.NewHistogram("requests"), Noroute: notFound, WSConn: stats.NewGauge("ws.conn"), StatusTimer: stats.NewHistogram("http.status", "code"), RedirectCounter: stats.NewCounter("http.redirect.count", "code"), } } var httpOnce sync.Once tcpCounters := func() { tcpConn = stats.NewCounter("tcp.conn") tcpConnFail = stats.NewCounter("tcp.connfail") tcpNoRoute = stats.NewCounter("tcp.noroute") } var tcpOnce sync.Once tcpSniCounters := func() { tcpSniConn = stats.NewCounter("tcp_sni.conn") tcpSniConnFail = stats.NewCounter("tcp_sni.connfail") tcpSniNoRoute = stats.NewCounter("tcp_sni.noroute") } var tcpSniOnce sync.Once for _, l := range cfg.Listen { l := l // capture loop var for go routines below tlscfg, err := makeTLSConfig(l) if err != nil { exit.Fatal("[FATAL] ", err) } log.Printf("[INFO] %s proxy listening on %s", strings.ToUpper(l.Proto), l.Addr) if tlscfg != nil && tlscfg.ClientAuth == tls.RequireAndVerifyClientCert { log.Printf("[INFO] Client certificate authentication enabled on %s", l.Addr) } switch l.Proto { case "http", "https": httpOnce.Do(httpCounters) go func() { h := newHTTPProxy(cfg, httpStatsHandler) // reset the ws.conn gauge h.Stats.WSConn.Set(0) if err := proxy.ListenAndServeHTTP(l, h, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() case "grpc", "grpcs": grpcOnce.Do(grpcCounters) go func() { h := newGrpcProxy(cfg, tlscfg, grpStatsHandler) if err := proxy.ListenAndServeGRPC(l, h, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() case "tcp": tcpOnce.Do(tcpCounters) go func() { h := &tcp.Proxy{ DialTimeout: cfg.Proxy.DialTimeout, Lookup: lookupHostFn(cfg, notFound), Conn: tcpConn, ConnFail: tcpConnFail, Noroute: tcpNoRoute, } if err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() case "tcp+sni": tcpSniOnce.Do(tcpSniCounters) go func() { h := &tcp.SNIProxy{ DialTimeout: cfg.Proxy.DialTimeout, Lookup: lookupHostFn(cfg, notFound), Conn: tcpSniConn, ConnFail: tcpSniConnFail, Noroute: tcpSniNoRoute, } if err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() case "tcp-dynamic": tcpOnce.Do(tcpCounters) go func() { var buffer strings.Builder lastPorts := []string{} for { time.Sleep(l.Refresh) table := route.GetTable() ports := []string{} for target, rts := range table { if strings.Contains(target, ":") { buffer.WriteString(":") buffer.WriteString(strings.Split(target, ":")[1]) schemes := tableSchemes(rts) if len(schemes) == 1 && schemes[0] == "tcp" { ports = append(ports, buffer.String()) } buffer.Reset() } ports = unique(ports) } for _, port := range difference(lastPorts, ports) { log.Printf("[DEBUG] Dynamic TCP listener on %s eligible for termination", port) proxy.CloseProxy(port) } for _, port := range ports { l := l port := port conn, err := net.Listen("tcp", port) if err != nil { log.Printf("[DEBUG] Dynamic TCP port %s in use", port) continue } conn.Close() log.Printf("[INFO] Starting dynamic TCP listener on port %s ", port) go func() { h := &tcp.DynamicProxy{ DialTimeout: cfg.Proxy.DialTimeout, Lookup: lookupHostFn(cfg, notFound), Conn: tcpConn, ConnFail: tcpConnFail, Noroute: tcpNoRoute, } l.Addr = port if err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() } lastPorts = ports } }() case "https+tcp+sni": tcpSniOnce.Do(tcpSniCounters) httpOnce.Do(httpCounters) go func() { hp := newHTTPProxy(cfg, httpStatsHandler) tp := &tcp.SNIProxy{ DialTimeout: cfg.Proxy.DialTimeout, Lookup: lookupHostFn(cfg, notFound), Conn: tcpSniConn, ConnFail: tcpSniConnFail, Noroute: tcpSniNoRoute} if err := proxy.ListenAndServeHTTPSTCPSNI(l, hp, tp, tlscfg, lookupHostMatcher(cfg)); err != nil { exit.Fatal("[FATAL] ", err) } }() case "prometheus": go func() { if err := proxy.ListenAndServePrometheus(l, cfg.Metrics.Prometheus, tlscfg); err != nil { exit.Fatal("[FATAL] ", err) } }() default: exit.Fatal("[FATAL] Invalid protocol ", l.Proto) } } } func startBGP(cfg *config.BGP) { h, err := bgp.NewBGPHandler(cfg) if err != nil { exit.Fatal("[FATAL] ", err) } go func() { if err := h.Start(); err != nil { exit.Fatal("[FATAL] ", err) } }() } func initRuntime(cfg *config.Config) { if os.Getenv("GOGC") == "" { log.Print("[INFO] Setting GOGC=", cfg.Runtime.GOGC) debug.SetGCPercent(cfg.Runtime.GOGC) } else { log.Print("[INFO] Using GOGC=", os.Getenv("GOGC"), " from env") } if os.Getenv("GOMAXPROCS") == "" { log.Print("[INFO] Setting GOMAXPROCS=", cfg.Runtime.GOMAXPROCS) runtime.GOMAXPROCS(cfg.Runtime.GOMAXPROCS) } else { log.Print("[INFO] Using GOMAXPROCS=", os.Getenv("GOMAXPROCS"), " from env") } } func initBackend(cfg *config.Config) { var deadline = time.Now().Add(cfg.Registry.Timeout) var err error for { switch cfg.Registry.Backend { case "file": registry.Default, err = file.NewBackend(&cfg.Registry.File) case "static": registry.Default, err = static.NewBackend(&cfg.Registry.Static) case "consul": registry.Default, err = consul.NewBackend(&cfg.Registry.Consul) case "custom": registry.Default, err = custom.NewBackend(&cfg.Registry.Custom) default: exit.Fatal("[FATAL] Unknown registry backend ", cfg.Registry.Backend) } if err == nil { if err = registry.Default.Register(nil); err == nil { return } } log.Print("[WARN] Error initializing backend. ", err) if time.Now().After(deadline) { exit.Fatal("[FATAL] Timeout registering backend.") } time.Sleep(cfg.Registry.Retry) if atomic.LoadInt32(&shuttingDown) > 0 { exit.Exit(1) } } } func watchBackend(cfg *config.Config, first chan bool) { var ( nextTable string lastTable string svccfg string mancfg string customBE string once sync.Once tableBuffer = new(bytes.Buffer) // fix crash on reset before used (#650) ) switch cfg.Registry.Backend { // custom back end receives JSON from a remote source that contains a slice of route.RouteDef // the route table is created directly from that input case "custom": svc := registry.Default.WatchServices() for { customBE = <-svc if customBE != "OK" { log.Printf("[ERROR] error during update from custom back end - %s", customBE) } once.Do(func() { close(first) }) } // all other backend types default: svc := registry.Default.WatchServices() man := registry.Default.WatchManual() for { select { case svccfg = <-svc: case mancfg = <-man: } // manual config overrides service config - order matters tableBuffer.Reset() tableBuffer.WriteString(svccfg) tableBuffer.WriteString("\n") tableBuffer.WriteString(mancfg) // set nextTable here to preserve the state. The buffer is altered // when calling route.NewTable and we lose change logging (#737) if nextTable = tableBuffer.String(); nextTable == lastTable { continue } aliases, err := route.ParseAliases(nextTable) if err != nil { log.Printf("[WARN]: %s", err) } registry.Default.Register(aliases) t, err := route.NewTable(tableBuffer) if err != nil { log.Printf("[WARN] %s", err) continue } route.SetTable(t) logRoutes(t, lastTable, nextTable, cfg.Log.RoutesFormat) lastTable = nextTable once.Do(func() { close(first) }) } } } func watchNoRouteHTML() { html := registry.Default.WatchNoRouteHTML() for { next := <-html if next == noroute.GetHTML() { continue } noroute.SetHTML(next) if next == "" { log.Print("[INFO] Unset noroute HTML") } else { log.Printf("[INFO] Set noroute HTML (%d bytes)", len(next)) } } } func logRoutes(t route.Table, last, next, format string) { fmtDiff := func(diffs []dmp.Diff) string { var b bytes.Buffer for _, d := range diffs { t := strings.TrimSpace(d.Text) if t == "" { continue } switch d.Type { case dmp.DiffDelete: b.WriteString("- ") b.WriteString(strings.ReplaceAll(t, "\n", "\n- ")) case dmp.DiffInsert: b.WriteString("+ ") b.WriteString(strings.ReplaceAll(t, "\n", "\n+ ")) } } return b.String() } const defFormat = "delta" switch format { case "detail": log.Printf("[INFO] Updated config to\n%s", t.Dump()) case "delta": if delta := fmtDiff(dmp.New().DiffMain(last, next, true)); delta != "" { log.Printf("[INFO] Config updates\n%s", delta) } case "all": log.Printf("[INFO] Updated config to\n%s", next) default: log.Printf("[WARN] Invalid route format %q. Defaulting to %q", format, defFormat) logRoutes(t, last, next, defFormat) } } func toJSON(v interface{}) string { data, err := json.MarshalIndent(v, "", " ") if err != nil { panic("json: " + err.Error()) } return string(data) } func unique(strSlice []string) []string { keys := make(map[string]bool) list := []string{} for _, entry := range strSlice { if _, value := keys[entry]; !value { keys[entry] = true list = append(list, entry) } } return list } // difference returns elements in `a` that aren't in `b` func difference(a, b []string) []string { mb := make(map[string]struct{}, len(b)) for _, x := range b { mb[x] = struct{}{} } var diff []string for _, x := range a { if _, found := mb[x]; !found { diff = append(diff, x) } } return diff } func tableSchemes(r route.Routes) []string { schemes := []string{} for _, rt := range r { for _, target := range rt.Targets { schemes = append(schemes, target.URL.Scheme) } } return unique(schemes) } ================================================ FILE: metrics/metrics.go ================================================ package metrics import ( "fmt" "log" "strings" "github.com/fabiolb/fabio/config" gkm "github.com/go-kit/kit/metrics" ) // Provider is an abstraction of a metrics backend. type Provider interface { // NewCounter creates a new counter object. NewCounter(name string, labels ...string) gkm.Counter // NewGauge creates a new gauge object. NewGauge(name string, labels ...string) gkm.Gauge // NewHistogram creates a new histogram object NewHistogram(name string, labels ...string) gkm.Histogram } // DeletableCounter is a counter that supports deleting label value combinations. // This is used to clean up stale metrics when routes are removed. type DeletableCounter interface { gkm.Counter // DeleteLabelValues removes the metric with the given label values. // Returns true if the metric was deleted. DeleteLabelValues(labelValues ...string) bool } // DeletableGauge is a gauge that supports deleting label value combinations. type DeletableGauge interface { gkm.Gauge // DeleteLabelValues removes the metric with the given label values. DeleteLabelValues(labelValues ...string) bool } // DeletableHistogram is a histogram that supports deleting label value combinations. type DeletableHistogram interface { gkm.Histogram // DeleteLabelValues removes the metric with the given label values. DeleteLabelValues(labelValues ...string) bool } func Initialize(cfg *config.Metrics) (Provider, error) { var p []Provider var prefix string var err error if prefix, err = parsePrefix(cfg.Prefix); err != nil { return nil, fmt.Errorf("metrics: invalid Prefix template: %w", err) } for _, x := range strings.Split(cfg.Target, ",") { x = strings.TrimSpace(x) switch x { case "flat", "stdout": p = append(p, &flatProvider{prefix}) case "label": p = append(p, &labelProvider{prefix}) case "statsd_raw": pp, err := NewStatsdProvider(prefix, cfg.StatsDAddr, cfg.Interval) if err != nil { return nil, err } p = append(p, pp) case "statsd": return nil, fmt.Errorf("statsd support has been removed in favor of statsd_raw") case "dogstatsd": pp, err := NewDogstatsdProvider(prefix, cfg.DogstatsdAddr, cfg.Interval) if err != nil { return nil, err } p = append(p, pp) case "prometheus": p = append(p, NewPromProvider(prefix, cfg.Prometheus.Subsystem, cfg.Prometheus.Buckets)) case "circonus": pp, err := NewCirconusProvider(prefix, cfg.Circonus, cfg.Interval) if err != nil { return nil, err } p = append(p, pp) case "graphite": pp, err := NewGraphiteProvider(prefix, cfg.GraphiteAddr, 50, cfg.Interval) if err != nil { return nil, err } p = append(p, pp) case "": // metrics are disabled. default: return nil, fmt.Errorf("invalid metrics backend %s", x) } log.Printf("[INFO] Registering metrics provider %q", x) if len(p) == 0 { log.Printf("[INFO] Metrics disabled") } } if len(p) == 0 { return &DiscardProvider{}, nil } return NewMultiProvider(p), nil } ================================================ FILE: metrics/names.go ================================================ package metrics import ( "bytes" "fmt" "net/url" "os" "path/filepath" "strings" "text/template" ) type Service struct { TargetURL *url.URL Service string Host string Path string } // DefaultNames contains the default template for route metric names for backends that don't // support tags. const DefaultNames = "{{clean .Service}}.{{clean .Host}}.{{clean .Path}}.{{clean .TargetURL.Host}}" // DefaulPrefix contains the default template for metrics prefix. const DefaultPrefix = "{{clean .Hostname}}.{{clean .Exec}}" // names stores the template for the route metric names. var names *template.Template func init() { // make sure names is initialized to something var err error if names, err = parseNames(DefaultNames); err != nil { panic(err) } } func (s Service) String() string { return s.Service } const DotSeparator = "." const PipeSeparator = "|" const RoutePrefix = "route" func Flatten(name string, labels []string, separator string) string { if len(labels) == 0 { return name } return name + separator + strings.Join(labels, separator) } func Labels(labels, values []string, stringsprefix, fieldsep, recsep string) string { if len(labels) == 0 { return "" } var b strings.Builder _, _ = b.WriteString(stringsprefix) for i := range labels { if i > 0 { _, _ = b.WriteString(recsep) } _, _ = b.WriteString(labels[i]) _, _ = b.WriteString(fieldsep) if i < len(values) { _, _ = b.WriteString(values[i]) } } return b.String() } // parseNames parses the route metric name template. func parseNames(tmpl string) (*template.Template, error) { funcMap := template.FuncMap{ "clean": clean, } t, err := template.New("names").Funcs(funcMap).Parse(tmpl) if err != nil { return nil, err } testURL, err := url.Parse("http://127.0.0.1:12345/") if err != nil { return nil, err } if _, err := TargetName("testservice", "test.example.com", "/test", testURL.String()); err != nil { return nil, err } return t, nil } // parsePrefix parses the prefix metric template func parsePrefix(tmpl string) (string, error) { // Backward compatibility condition for old metrics.prefix parameter 'default' if tmpl == "default" { tmpl = DefaultPrefix } funcMap := template.FuncMap{ "clean": clean, } t, err := template.New("prefix").Funcs(funcMap).Parse(tmpl) if err != nil { return "", err } host, err := hostname() if err != nil { return "", err } exe := filepath.Base(os.Args[0]) b := new(bytes.Buffer) data := struct{ Hostname, Exec string }{host, exe} if err := t.Execute(b, &data); err != nil { return "", err } return b.String(), nil } // clean creates safe names for graphite reporting by replacing // some characters with underscores. // TODO(fs): This may need updating for other metrics backends. func clean(s string) string { if s == "" { return "_" } s = strings.ReplaceAll(s, ".", "_") s = strings.ReplaceAll(s, ":", "_") return strings.ToLower(s) } // stubbed out for testing var hostname = os.Hostname // TargetName returns the metrics name from the given parameters. func TargetName(service, host, path, target string) (string, error) { if names == nil { return "", nil } targetURL, err := url.Parse(target) if err != nil { return "", fmt.Errorf("error parsing URL %s: %w", target, err) } var name bytes.Buffer data := struct { TargetURL *url.URL Service, Host, Path string }{targetURL, service, host, path} if err := names.Execute(&name, data); err != nil { return "", err } return name.String(), nil } // TargetNameWith - this is used for flat metrics backends (no tags support) // in With() methods on target metrics. func TargetNameWith(name string, values []string) (string, error) { if len(values)%2 == 1 { values = append(values, "unknown") } m := make(map[string]string) for i := 0; i < len(values); i += 2 { m[values[i]] = values[i+1] } n, err := TargetName(m["service"], m["host"], m["path"], m["target"]) if err != nil { return "", err } // Handle .tx and .rx if i := strings.LastIndex(name, "."); i != -1 { n += name[i:] } return n, nil } func isRouteMetric(name string) bool { return strings.HasPrefix(name, RoutePrefix) } ================================================ FILE: metrics/names_test.go ================================================ package metrics import ( "os" "testing" ) func TestParsePrefix(t *testing.T) { hostname = func() (string, error) { return "myhost", nil } os.Args = []string{"./myapp"} got, err := parsePrefix("{{clean .Hostname}}.{{clean .Exec}}") if err != nil { t.Fatalf("%v", err) } want := "myhost.myapp" if got != want { t.Errorf("ParsePrefix: got %v want %v", got, want) } got, err = parsePrefix("default") if err != nil { t.Fatalf("%v", err) } want = "myhost.myapp" if got != want { t.Errorf("ParsePrefix Old default style: got %v want %v", got, want) } } func TestTargetName(t *testing.T) { tests := []struct { service, host, path, target string name string }{ {"s", "h", "p", "http://foo.com/bar", "s.h.p.foo_com"}, {"s", "", "p", "http://foo.com/bar", "s._.p.foo_com"}, {"s", "", "", "http://foo.com/bar", "s._._.foo_com"}, {"", "", "", "http://foo.com/bar", "_._._.foo_com"}, {"", "", "", "http://foo.com:1234/bar", "_._._.foo_com_1234"}, {"", "", "", "http://1.2.3.4:1234/bar", "_._._.1_2_3_4_1234"}, } for i, tt := range tests { got, err := TargetName(tt.service, tt.host, tt.path, tt.target) if err != nil { t.Fatalf("%d: %v", i, err) } if want := tt.name; got != want { t.Errorf("%d: got %q want %q", i, got, want) } } } ================================================ FILE: metrics/provider_circonus.go ================================================ package metrics import ( "errors" "fmt" "log" "os" "sync" "time" cgm "github.com/circonus-labs/circonus-gometrics/v3" "github.com/fabiolb/fabio/config" gkm "github.com/go-kit/kit/metrics" ) var ( circonus *CirconusProvider circOnce sync.Once ) const serviceName = "fabio" func NewCirconusProvider(prefix string, circ config.Circonus, interval time.Duration) (*CirconusProvider, error) { var initError error circOnce.Do(func() { if circ.APIKey == "" && circ.SubmissionURL == "" { initError = errors.New("metrics: Circonus API token key or SubmissionURL") return } if circ.APIApp == "" { circ.APIApp = serviceName } host, err := os.Hostname() if err != nil { initError = fmt.Errorf("metrics: unable to initialize Circonus %s", err) return } cfg := &cgm.Config{} cfg.CheckManager.Check.SubmissionURL = circ.SubmissionURL cfg.CheckManager.API.TokenKey = circ.APIKey cfg.CheckManager.API.TokenApp = circ.APIApp cfg.CheckManager.API.URL = circ.APIURL cfg.CheckManager.Check.ID = circ.CheckID cfg.CheckManager.Broker.ID = circ.BrokerID cfg.Interval = fmt.Sprintf("%.0fs", interval.Seconds()) cfg.CheckManager.Check.InstanceID = host cfg.CheckManager.Check.DisplayName = fmt.Sprintf("%s /%s", host, serviceName) cfg.CheckManager.Check.SearchTag = fmt.Sprintf("service:%s", serviceName) metrics, err := cgm.NewCirconusMetrics(cfg) if err != nil { initError = fmt.Errorf("metrics: unable to initialize Circonus %s", err) return } circonus = &CirconusProvider{metrics, prefix} metrics.Start() log.Print("[INFO] Sending metrics to Circonus") }) return circonus, initError } type CirconusProvider struct { metrics *cgm.CirconusMetrics prefix string } func (cp *CirconusProvider) metricName(name string) string { return fmt.Sprintf("%s`%s", cp.prefix, name) } func (cp *CirconusProvider) NewCounter(name string, labels ...string) gkm.Counter { return &cgmCounter{ p: cp, name: cp.metricName(name), routeMetric: isRouteMetric(name), } } func (cp *CirconusProvider) NewGauge(name string, labels ...string) gkm.Gauge { return &cgmGauge{ p: cp, name: cp.metricName(name), routeMetric: isRouteMetric(name), } } func (cp *CirconusProvider) NewHistogram(name string, labels ...string) gkm.Histogram { return &cgmTimer{ p: cp, name: cp.metricName(name), routeMetric: isRouteMetric(name), } } type cgmCounter struct { p *CirconusProvider name string routeMetric bool } func (c *cgmCounter) With(labelValues ...string) gkm.Counter { var name string switch c.routeMetric { case true: var err error name, err = TargetNameWith(c.name, labelValues) if err != nil { panic(err) } name = c.p.metricName(name) case false: name = Flatten(c.name, labelValues, DotSeparator) } return &cgmCounter{ p: c.p, name: name, routeMetric: c.routeMetric, } } func (c *cgmCounter) Add(delta float64) { c.p.metrics.IncrementByValue(c.name, uint64(delta)) } type cgmGauge struct { p *CirconusProvider name string routeMetric bool } func (g *cgmGauge) With(labelValues ...string) gkm.Gauge { var name string switch g.routeMetric { case true: var err error name, err = TargetNameWith(g.name, labelValues) if err != nil { panic(err) } name = g.p.metricName(name) case false: name = Flatten(g.name, labelValues, DotSeparator) } return &cgmGauge{ p: g.p, name: name, routeMetric: g.routeMetric, } } func (g *cgmGauge) Set(value float64) { g.p.metrics.Gauge(g.name, value) } func (g *cgmGauge) Add(delta float64) { g.p.metrics.AddGauge(g.name, delta) } type cgmTimer struct { p *CirconusProvider name string routeMetric bool } func (t *cgmTimer) With(labelValues ...string) gkm.Histogram { var name string switch t.routeMetric { case true: var err error name, err = TargetNameWith(t.name, labelValues) if err != nil { panic(err) } name = t.p.metricName(name) case false: name = Flatten(t.name, labelValues, DotSeparator) } return &cgmTimer{ p: t.p, name: name, routeMetric: t.routeMetric, } } func (t *cgmTimer) Observe(value float64) { t.p.metrics.Timing(t.name, value*float64(time.Second)) } ================================================ FILE: metrics/provider_circonus_test.go ================================================ package metrics import ( "os" "testing" "time" "github.com/fabiolb/fabio/config" ) func TestAll(t *testing.T) { start := time.Now() if os.Getenv("CIRCONUS_API_TOKEN") == "" && os.Getenv("CIRCONUS_SUBMISSION_URL") == "" { t.Skip("skipping test; $CIRCONUS_API_TOKEN or $CIRCONUS_SUBMISSION_URL not set") } t.Log("Testing cgm functionality -- this *will* create/use a check") cfg := config.Circonus{ SubmissionURL: os.Getenv("CIRCONUS_SUBMISSION_URL"), APIKey: os.Getenv("CIRCONUS_API_TOKEN"), APIApp: os.Getenv("CIRCONUS_API_APP"), APIURL: os.Getenv("CIRCONUS_API_URL"), CheckID: os.Getenv("CIRCONUS_CHECK_ID"), BrokerID: os.Getenv("CIRCONUS_BROKER_ID"), } interval, err := time.ParseDuration("60s") if err != nil { t.Fatalf("Unable to parse interval %+v", err) } circ, err := NewCirconusProvider("test", cfg, interval) if err != nil { t.Fatalf("Unable to initialize Circonus +%v", err) } counter := circ.NewCounter("fooCounter") counter.Add(3) timer := circ.NewHistogram("fooTimer") timer.Observe(time.Since(start).Seconds()) circonus.metrics.Flush() } ================================================ FILE: metrics/provider_discard.go ================================================ package metrics import ( gkm "github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics/discard" ) type DiscardProvider struct{} func (dp DiscardProvider) NewCounter(name string, labels ...string) gkm.Counter { return discard.NewCounter() } func (dp DiscardProvider) NewGauge(name string, labels ...string) gkm.Gauge { return discard.NewGauge() } func (dp DiscardProvider) NewHistogram(name string, labels ...string) gkm.Histogram { return discard.NewHistogram() } ================================================ FILE: metrics/provider_dogstatsd.go ================================================ package metrics import ( "context" "fmt" "net" "time" gkm "github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics/dogstatsd" "github.com/go-kit/log" ) type DogstatsdProvider struct { D *dogstatsd.Dogstatsd } func (dp *DogstatsdProvider) NewCounter(name string, labels ...string) gkm.Counter { return &dogstatsdCounter{dp.D.NewCounter(name, 1)} } func (dp *DogstatsdProvider) NewGauge(name string, labels ...string) gkm.Gauge { return &dogstatsdGauge{dp.D.NewGauge(name)} } func (dp *DogstatsdProvider) NewHistogram(name string, labels ...string) gkm.Histogram { return &dogstatsdHistogram{dp.D.NewHistogram(name, 1)} } func NewDogstatsdProvider(prefix, addr string, interval time.Duration) (*DogstatsdProvider, error) { d := &DogstatsdProvider{ D: dogstatsd.New(prefix, log.NewNopLogger()), } _, err := net.ResolveUDPAddr("udp", addr) if err != nil { return nil, fmt.Errorf("error resolving dogstatsd address %s: %w", addr, err) } t := time.NewTicker(interval) go func() { d.D.SendLoop(context.Background(), t.C, "udp", addr) }() return d, nil } type dogstatsdCounter struct { gkm.Counter } type dogstatsdGauge struct { gkm.Gauge } type dogstatsdHistogram struct { gkm.Histogram } func (dh *dogstatsdHistogram) Observe(value float64) { dh.Histogram.Observe(value * 1000.0) } func (dh *dogstatsdCounter) With(labelValues ...string) gkm.Counter { return dh.Counter.With(correctReservedTagKeys(labelValues)...) } func (dh *dogstatsdGauge) With(labelValues ...string) gkm.Gauge { return dh.Gauge.With(correctReservedTagKeys(labelValues)...) } func (dh *dogstatsdHistogram) With(labelValues ...string) gkm.Histogram { return dh.Histogram.With(correctReservedTagKeys(labelValues)...) } func correctReservedTagKeys(labelValues []string) []string { var rval []string for i, v := range labelValues { if i%2 == 0 { rval = append(rval, correctReservedTagKey(v)) } else { rval = append(rval, v) } } return rval } func correctReservedTagKey(label string) string { switch label { case "host": return "fabio-host" case "device": return "fabio-device" case "source": return "fabio-source" case "service": return "fabio-service" case "env": return "fabio-env" case "version": return "fabio-version" default: return label } } ================================================ FILE: metrics/provider_dogstatsd_test.go ================================================ package metrics import ( "bytes" "github.com/go-kit/kit/metrics/dogstatsd" "github.com/go-kit/log" "reflect" "testing" "time" ) func TestDogstatsdProvider(t *testing.T) { prefix := "test-" d := dogstatsd.New(prefix, log.NewNopLogger()) provider := &DogstatsdProvider{D: d} for _, tst := range []struct { name string labels []string values []string expected_values []string prefix string countval float64 gaugeval float64 histoval float64 }{ { name: "simpleTest", labels: []string{"service", "host", "path", "target", "other"}, values: []string{"service", "foo", "host", "bar", "path", "/asdf", "target", "http://jkl.org:1234", "other", "trailer"}, expected_values: []string{"fabio-service", "foo", "fabio-host", "bar", "path", "/asdf", "target", "http://jkl.org:1234", "other", "trailer"}, prefix: "tst", countval: 20, gaugeval: 30, histoval: (time.Microsecond * 50).Seconds(), }, } { t.Run(tst.name, func(t *testing.T) { counter := provider.NewCounter(tst.prefix+".counter", tst.labels...) gauge := provider.NewGauge(tst.prefix+".gauge", tst.labels...) histo := provider.NewHistogram(tst.prefix+".histogram", tst.labels...) if len(tst.labels) > 0 { counter = counter.With(tst.values...) gauge = gauge.With(tst.values...) histo = histo.With(tst.values...) } counter.Add(tst.countval) gauge.Set(tst.gaugeval) histo.Observe(tst.histoval) var buff bytes.Buffer _, _ = provider.D.WriteTo(&buff) m := parseStatsdMetrics(&buff, prefix) for _, v := range []struct { n string v float64 }{ {tst.prefix + ".counter", tst.countval}, {tst.prefix + ".gauge", tst.gaugeval}, {tst.prefix + ".histogram", tst.histoval}, } { if se, ok := m[v.n]; ok { if se.value != v.v { t.Errorf("%s failed: expected: %.02f, got %02f", v.n, v.v, se.value) } if len(tst.expected_values) > 0 && !reflect.DeepEqual(se.tags, tst.expected_values) { t.Errorf("tags did not survive round trip parsing") } } else { t.Errorf("%s not found", v.n) } } }) } } ================================================ FILE: metrics/provider_flat.go ================================================ package metrics import ( "fmt" "math" "strings" "sync/atomic" gkm "github.com/go-kit/kit/metrics" ) type flatProvider struct { prefix string } func (p *flatProvider) NewCounter(name string, labels ...string) gkm.Counter { return &flatCounter{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)} } func (p *flatProvider) NewGauge(name string, labels ...string) gkm.Gauge { return &flatGauge{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)} } func (p *flatProvider) NewHistogram(name string, labels ...string) gkm.Histogram { return &flatHistogram{Name: Flatten(strings.Join([]string{p.prefix, name}, DotSeparator), labels, DotSeparator)} } type flatCounter struct { Name string v uint64 } func (c *flatCounter) With(labelValues ...string) gkm.Counter { return c } func (c *flatCounter) Add(v float64) { uv := atomic.AddUint64(&c.v, uint64(v)) fmt.Printf("%s:%d|c\n", c.Name, uv) } type flatGauge struct { Name string // Stolen from prometheus client gauge valBits uint64 } func (g *flatGauge) Set(n float64) { atomic.StoreUint64(&g.valBits, math.Float64bits(n)) fmt.Printf("%s:%d|g\n", g.Name, int(n)) } func (g *flatGauge) Add(delta float64) { var oldBits uint64 var newBits uint64 for { oldBits = atomic.LoadUint64(&g.valBits) newBits = math.Float64bits(math.Float64frombits(oldBits) + delta) if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) { break } } fmt.Printf("%s:%d|g\n", g.Name, int(math.Float64frombits(newBits))) } func (g *flatGauge) With(labelValues ...string) gkm.Gauge { return g } type flatHistogram struct { Name string } func (h *flatHistogram) Observe(t float64) { fmt.Printf(":%s:%d|ms\n", h.Name, int64(math.Round(t*1000.0))) } func (h *flatHistogram) With(labels ...string) gkm.Histogram { return h } ================================================ FILE: metrics/provider_graphite.go ================================================ package metrics import ( "context" "fmt" gkm "github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics/graphite" "github.com/go-kit/log" "net" "time" ) type GraphiteProvider struct { G *graphite.Graphite buckets int } func (g *GraphiteProvider) NewCounter(name string, labels ...string) gkm.Counter { if len(labels) == 0 { return g.G.NewCounter(name) } return &graphiteCounter{ p: g, name: name, routeMetric: isRouteMetric(name), } } func (g *GraphiteProvider) NewGauge(name string, labels ...string) gkm.Gauge { if len(labels) == 0 { return g.G.NewGauge(name) } return &graphiteGauge{ p: g, name: name, routeMetric: isRouteMetric(name), } } func (g *GraphiteProvider) NewHistogram(name string, labels ...string) gkm.Histogram { var histogram gkm.Histogram if len(labels) == 0 { histogram = g.G.NewHistogram(name, g.buckets) } return &graphiteHistogram{ Histogram: histogram, p: g, name: name, routeMetric: isRouteMetric(name), } } func NewGraphiteProvider(prefix, addr string, buckets int, interval time.Duration) (*GraphiteProvider, error) { g := &GraphiteProvider{ G: graphite.New(prefix, log.NewNopLogger()), buckets: buckets, } _, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return nil, fmt.Errorf("error resolving graphite address %s: %w", addr, err) } t := time.NewTicker(interval) go func() { g.G.SendLoop(context.Background(), t.C, "tcp", addr) }() return g, nil } type graphiteCounter struct { gkm.Counter p *GraphiteProvider name string routeMetric bool } func (c *graphiteCounter) With(labelValues ...string) gkm.Counter { var name string switch c.routeMetric { case true: var err error name, err = TargetNameWith(c.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(c.name, labelValues, DotSeparator) } return &graphiteCounter{ Counter: c.p.G.NewCounter(name), name: name, p: c.p, routeMetric: c.routeMetric, } } type graphiteGauge struct { gkm.Gauge p *GraphiteProvider name string routeMetric bool } func (g *graphiteGauge) With(labelValues ...string) gkm.Gauge { var name string switch g.routeMetric { case true: var err error name, err = TargetNameWith(g.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(g.name, labelValues, DotSeparator) } return &graphiteGauge{ Gauge: g.p.G.NewGauge(name), name: name, p: g.p, routeMetric: g.routeMetric, } } type graphiteHistogram struct { gkm.Histogram p *GraphiteProvider name string routeMetric bool } func (h *graphiteHistogram) With(labelValues ...string) gkm.Histogram { var name string switch h.routeMetric { case true: var err error name, err = TargetNameWith(h.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(h.name, labelValues, DotSeparator) } return &graphiteHistogram{ Histogram: h.p.G.NewHistogram(name, h.p.buckets), name: name, p: h.p, routeMetric: h.routeMetric, } } func (h *graphiteHistogram) Observe(value float64) { h.Histogram.Observe(value * 1000.0) } ================================================ FILE: metrics/provider_label.go ================================================ package metrics import ( "fmt" gkm "github.com/go-kit/kit/metrics" "math" "strings" "sync/atomic" ) type labelProvider struct { prefix string } func (p *labelProvider) NewCounter(name string, labels ...string) gkm.Counter { return &labelCounter{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels} } func (p *labelProvider) NewGauge(name string, labels ...string) gkm.Gauge { return &labelGauge{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels} } func (p *labelProvider) NewHistogram(name string, labels ...string) gkm.Histogram { return &labelHistogram{Name: strings.Join([]string{p.prefix, name}, DotSeparator), Labels: labels} } type labelCounter struct { Name string Labels []string Values []string v int64 } func (c *labelCounter) With(labelValues ...string) gkm.Counter { cc := &labelCounter{ Name: c.Name, Labels: c.Labels, Values: make([]string, len(labelValues)), v: c.v, } copy(cc.Values, labelValues) return cc } func (c *labelCounter) Inc() { v := atomic.AddInt64(&c.v, 1) fmt.Printf("%s:%d|c%s\n", c.Name, v, Labels(c.Labels, c.Values, "|#", ":", ",")) } func (c *labelCounter) Add(delta float64) { v := atomic.AddInt64(&c.v, int64(delta)) fmt.Printf("%s:%d|c%s\n", c.Name, v, Labels(c.Labels, c.Values, "|#", ":", ",")) } type labelGauge struct { Name string Labels []string Values []string valBits uint64 } func (g *labelGauge) With(labelValues ...string) gkm.Gauge { gc := &labelGauge{ Name: g.Name, Labels: g.Labels, Values: make([]string, len(labelValues)), } copy(gc.Values, labelValues) return gc } func (g *labelGauge) Set(n float64) { atomic.StoreUint64(&g.valBits, math.Float64bits(n)) fmt.Printf("%s:%d|g%s\n", g.Name, int(n), Labels(g.Labels, g.Values, "|#", ":", ",")) } func (g *labelGauge) Add(delta float64) { var oldBits uint64 var newBits uint64 for { oldBits = atomic.LoadUint64(&g.valBits) newBits = math.Float64bits(math.Float64frombits(oldBits) + delta) if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) { break } } fmt.Printf("%s:%d|g%s\n", g.Name, int(delta), Labels(g.Labels, g.Values, "|#", ":", ",")) } type labelHistogram struct { Name string Labels []string Values []string } func (h *labelHistogram) With(labels ...string) gkm.Histogram { h2 := &labelHistogram{} *h2 = *h h2.Values = make([]string, len(labels)) copy(h2.Values, labels) return h2 } func (h *labelHistogram) Observe(t float64) { fmt.Printf("%s:%d|ms%s\n", h.Name, int64(math.Round(t*1000.0)), Labels(h.Labels, h.Values, "|#", ":", ",")) } ================================================ FILE: metrics/provider_multi.go ================================================ package metrics import gkm "github.com/go-kit/kit/metrics" // MultiProvider wraps zero or more providers. type MultiProvider struct { p []Provider } func NewMultiProvider(p []Provider) *MultiProvider { return &MultiProvider{p} } // NewCounter creates a MultiCounter with counter objects for all registered // providers. func (mp *MultiProvider) NewCounter(name string, labels ...string) gkm.Counter { var c []gkm.Counter for _, p := range mp.p { c = append(c, p.NewCounter(name, labels...)) } return &MultiCounter{c} } // NewGauge creates a MultiGauge with gauge objects for all registered // providers. func (mp *MultiProvider) NewGauge(name string, labels ...string) gkm.Gauge { var v []gkm.Gauge for _, p := range mp.p { v = append(v, p.NewGauge(name, labels...)) } return &MultiGauge{v} } func (mp *MultiProvider) NewHistogram(name string, labels ...string) gkm.Histogram { var h []gkm.Histogram for _, p := range mp.p { h = append(h, p.NewHistogram(name, labels...)) } return &MultiHistogram{h: h} } // MultiCounter wraps zero or more counters. type MultiCounter struct { c []gkm.Counter } func (mc *MultiCounter) Add(v float64) { for _, c := range mc.c { c.Add(v) } } func (mc *MultiCounter) With(labels ...string) gkm.Counter { cc := make([]gkm.Counter, len(mc.c)) for i := range mc.c { cc[i] = mc.c[i].With(labels...) } return &MultiCounter{c: cc} } // DeleteLabelValues deletes the metric with the given label values from all underlying counters. func (mc *MultiCounter) DeleteLabelValues(labelValues ...string) bool { deleted := false for _, c := range mc.c { if dc, ok := c.(DeletableCounter); ok { if dc.DeleteLabelValues(labelValues...) { deleted = true } } } return deleted } // MultiGauge wraps zero or more gauges. type MultiGauge struct { v []gkm.Gauge } func (m *MultiGauge) Set(n float64) { for _, v := range m.v { v.Set(n) } } func (m *MultiGauge) With(labels ...string) gkm.Gauge { vc := make([]gkm.Gauge, len(m.v)) for i := range m.v { vc[i] = m.v[i].With(labels...) } return &MultiGauge{v: vc} } func (m *MultiGauge) Add(val float64) { for _, v := range m.v { v.Add(val) } } // DeleteLabelValues deletes the metric with the given label values from all underlying gauges. func (m *MultiGauge) DeleteLabelValues(labelValues ...string) bool { deleted := false for _, g := range m.v { if dg, ok := g.(DeletableGauge); ok { if dg.DeleteLabelValues(labelValues...) { deleted = true } } } return deleted } type MultiHistogram struct { h []gkm.Histogram } func (m *MultiHistogram) With(labelValues ...string) gkm.Histogram { hc := make([]gkm.Histogram, len(m.h)) for i := range m.h { hc[i] = m.h[i].With(labelValues...) } return &MultiHistogram{h: hc} } func (m *MultiHistogram) Observe(value float64) { for _, v := range m.h { v.Observe(value) } } // DeleteLabelValues deletes the metric with the given label values from all underlying histograms. func (m *MultiHistogram) DeleteLabelValues(labelValues ...string) bool { deleted := false for _, h := range m.h { if dh, ok := h.(DeletableHistogram); ok { if dh.DeleteLabelValues(labelValues...) { deleted = true } } } return deleted } ================================================ FILE: metrics/provider_prometheus.go ================================================ package metrics import ( gkm "github.com/go-kit/kit/metrics" promclient "github.com/prometheus/client_golang/prometheus" ) type PromProvider struct { Opts promclient.Opts Buckets []float64 } func NewPromProvider(namespace, subsystem string, buckets []float64) Provider { namespace = clean(namespace) if len(subsystem) > 0 { subsystem = clean(subsystem) } return &PromProvider{ Opts: promclient.Opts{ Namespace: namespace, Subsystem: subsystem, }, Buckets: buckets, } } func (p *PromProvider) NewCounter(name string, labels ...string) gkm.Counter { copts := promclient.CounterOpts(p.Opts) copts.Name = clean(name) cv := promclient.NewCounterVec(copts, labels) promclient.MustRegister(cv) return &promCounter{cv: cv} } func (p *PromProvider) NewGauge(name string, labels ...string) gkm.Gauge { gopts := promclient.GaugeOpts(p.Opts) gopts.Name = clean(name) gv := promclient.NewGaugeVec(gopts, labels) promclient.MustRegister(gv) return &promGauge{gv: gv} } func (p *PromProvider) NewHistogram(name string, labels ...string) gkm.Histogram { hopts := promclient.HistogramOpts{ Namespace: p.Opts.Namespace, Subsystem: p.Opts.Subsystem, Name: clean(name), Help: p.Opts.Help, ConstLabels: p.Opts.ConstLabels, Buckets: p.Buckets, } hv := promclient.NewHistogramVec(hopts, labels) promclient.MustRegister(hv) return &promHistogram{hv: hv} } // makeLabels converts a slice of alternating key-value pairs into prometheus.Labels. // e.g., ["service", "foo", "host", "bar"] -> {"service": "foo", "host": "bar"} func makeLabels(lvs []string) promclient.Labels { labels := promclient.Labels{} for i := 0; i < len(lvs); i += 2 { labels[lvs[i]] = lvs[i+1] } return labels } // promCounter wraps a Prometheus CounterVec and supports deletion of label values. type promCounter struct { cv *promclient.CounterVec lvs []string // alternating key-value pairs } func (c *promCounter) Add(delta float64) { c.cv.With(makeLabels(c.lvs)).Add(delta) } func (c *promCounter) With(labelValues ...string) gkm.Counter { return &promCounter{ cv: c.cv, lvs: append(append([]string{}, c.lvs...), labelValues...), } } // DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs). func (c *promCounter) DeleteLabelValues(labelValues ...string) bool { return c.cv.DeleteLabelValues(labelValues...) } // promGauge wraps a Prometheus GaugeVec and supports deletion of label values. type promGauge struct { gv *promclient.GaugeVec lvs []string // alternating key-value pairs } func (g *promGauge) Set(value float64) { g.gv.With(makeLabels(g.lvs)).Set(value) } func (g *promGauge) Add(delta float64) { g.gv.With(makeLabels(g.lvs)).Add(delta) } func (g *promGauge) With(labelValues ...string) gkm.Gauge { return &promGauge{ gv: g.gv, lvs: append(append([]string{}, g.lvs...), labelValues...), } } // DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs). func (g *promGauge) DeleteLabelValues(labelValues ...string) bool { return g.gv.DeleteLabelValues(labelValues...) } // promHistogram wraps a Prometheus HistogramVec and supports deletion of label values. type promHistogram struct { hv *promclient.HistogramVec lvs []string // alternating key-value pairs } func (h *promHistogram) Observe(value float64) { h.hv.With(makeLabels(h.lvs)).Observe(value) } func (h *promHistogram) With(labelValues ...string) gkm.Histogram { return &promHistogram{ hv: h.hv, lvs: append(append([]string{}, h.lvs...), labelValues...), } } // DeleteLabelValues removes the metric with the given label values (values only, not key-value pairs). func (h *promHistogram) DeleteLabelValues(labelValues ...string) bool { return h.hv.DeleteLabelValues(labelValues...) } ================================================ FILE: metrics/provider_prometheus_test.go ================================================ package metrics import ( "strings" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" ) func TestPromProviderDeleteLabelValues(t *testing.T) { // Create a new registry to avoid conflicts with other tests reg := prometheus.NewRegistry() // Create histogram directly (not via provider to control registration) hv := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "test", Name: "route", Help: "test histogram", Buckets: prometheus.DefBuckets, }, []string{"service", "host", "path", "target"}) reg.MustRegister(hv) ph := &promHistogram{hv: hv} // Create metrics for two targets h1 := ph.With("service", "svc1", "host", "host1", "path", "/path1", "target", "http://target1/") h2 := ph.With("service", "svc2", "host", "host2", "path", "/path2", "target", "http://target2/") // Observe some values h1.Observe(0.1) h1.Observe(0.2) h2.Observe(0.3) // Verify both metrics exist count := testutil.CollectAndCount(hv) t.Logf("Metric count before delete: %d", count) if count == 0 { t.Fatal("Expected metrics to be registered") } // Gather metrics to check labels metrics, _ := reg.Gather() t.Logf("Metrics before delete:") for _, m := range metrics { for _, metric := range m.GetMetric() { var labels []string for _, l := range metric.GetLabel() { labels = append(labels, l.GetName()+"="+l.GetValue()) } t.Logf(" %s{%s}", m.GetName(), strings.Join(labels, ", ")) } } // Delete the first target's metrics (note: values only, not key-value pairs) deleted := ph.DeleteLabelValues("svc1", "host1", "/path1", "http://target1/") t.Logf("DeleteLabelValues returned: %v", deleted) // Gather metrics after delete metrics, _ = reg.Gather() t.Logf("Metrics after delete:") foundSvc1 := false foundSvc2 := false for _, m := range metrics { for _, metric := range m.GetMetric() { var labels []string for _, l := range metric.GetLabel() { labels = append(labels, l.GetName()+"="+l.GetValue()) if l.GetName() == "service" && l.GetValue() == "svc1" { foundSvc1 = true } if l.GetName() == "service" && l.GetValue() == "svc2" { foundSvc2 = true } } t.Logf(" %s{%s}", m.GetName(), strings.Join(labels, ", ")) } } if foundSvc1 { t.Error("svc1 metrics should have been deleted but were still found") } if !foundSvc2 { t.Error("svc2 metrics should still exist but were not found") } } func TestPromHistogramWithLabelValues(t *testing.T) { // Test that With() correctly accumulates label values hv := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "test2", Name: "route2", Help: "test histogram", Buckets: prometheus.DefBuckets, }, []string{"service", "host", "path", "target"}) ph := &promHistogram{hv: hv} // Add labels in stages like the real code does h := ph.With("service", "mysvc", "host", "myhost", "path", "/mypath", "target", "http://mytarget/") // Check internal state ph2 := h.(*promHistogram) t.Logf("lvs after With: %v", ph2.lvs) t.Logf("lvs length: %d", len(ph2.lvs)) expectedLvs := []string{"service", "mysvc", "host", "myhost", "path", "/mypath", "target", "http://mytarget/"} if len(ph2.lvs) != len(expectedLvs) { t.Errorf("Expected lvs length %d, got %d", len(expectedLvs), len(ph2.lvs)) } // Try to observe - this should not panic h.Observe(0.5) t.Log("Observe succeeded") } ================================================ FILE: metrics/provider_statsd.go ================================================ package metrics import ( "context" "fmt" "net" "time" gkm "github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics/statsd" "github.com/go-kit/log" ) type StatsdProvider struct { S *statsd.Statsd } func NewStatsdProvider(prefix, addr string, interval time.Duration) (*StatsdProvider, error) { p := &StatsdProvider{ S: statsd.New(prefix, log.NewNopLogger()), } _, err := net.ResolveUDPAddr("udp", addr) if err != nil { return nil, fmt.Errorf("error resolving statsd address %s: %w", addr, err) } t := time.NewTicker(interval) go func() { p.S.SendLoop(context.Background(), t.C, "udp", addr) }() return p, nil } // NewCounter - This assumes if there are labels, there will be a With() call func (p *StatsdProvider) NewCounter(name string, labels ...string) gkm.Counter { if len(labels) == 0 { return p.S.NewCounter(name, 1) } return &statsdCounter{ name: name, p: p, routeMetric: isRouteMetric(name), } } // NewGauge - this assumes if there are labels, there will be a With() call. func (p *StatsdProvider) NewGauge(name string, labels ...string) gkm.Gauge { if len(labels) == 0 { return p.S.NewGauge(name) } return &statsdGauge{ name: name, p: p, routeMetric: isRouteMetric(name), } } // NewHistogram - this assumes if there are labels, there will be a With() call. func (p *StatsdProvider) NewHistogram(name string, labels ...string) gkm.Histogram { var histogram gkm.Histogram if len(labels) == 0 { histogram = p.S.NewTiming(name, 1) } return &statsdHistogram{ Histogram: histogram, name: name, p: p, routeMetric: isRouteMetric(name), } } type statsdCounter struct { gkm.Counter p *StatsdProvider name string routeMetric bool } func (c *statsdCounter) With(labelValues ...string) gkm.Counter { var name string switch c.routeMetric { case true: var err error name, err = TargetNameWith(c.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(c.name, labelValues, DotSeparator) } return &statsdCounter{ Counter: c.p.S.NewCounter(name, 1), name: name, p: c.p, routeMetric: c.routeMetric, } } type statsdGauge struct { gkm.Gauge p *StatsdProvider name string routeMetric bool } func (g *statsdGauge) With(labelValues ...string) gkm.Gauge { var name string switch g.routeMetric { case true: var err error name, err = TargetNameWith(g.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(g.name, labelValues, DotSeparator) } return &statsdGauge{ Gauge: g.p.S.NewGauge(name), name: name, p: g.p, routeMetric: g.routeMetric, } } type statsdHistogram struct { gkm.Histogram p *StatsdProvider name string routeMetric bool } func (h *statsdHistogram) With(labelValues ...string) gkm.Histogram { var name string switch h.routeMetric { case true: var err error name, err = TargetNameWith(h.name, labelValues) if err != nil { panic(err) } case false: name = Flatten(h.name, labelValues, DotSeparator) } return &statsdHistogram{ Histogram: h.p.S.NewTiming(name, 1), name: name, p: h.p, routeMetric: h.routeMetric, } } func (h *statsdHistogram) Observe(value float64) { h.Histogram.Observe(value * 1000.0) } ================================================ FILE: metrics/provider_statsd_test.go ================================================ package metrics import ( "bufio" "bytes" "github.com/go-kit/kit/metrics/statsd" "github.com/go-kit/log" "io" "regexp" "strconv" "strings" "testing" "time" ) func TestStatsdProvider(t *testing.T) { prefix := "test-" s := statsd.New(prefix, log.NewNopLogger()) provider := &StatsdProvider{S: s} for _, tst := range []struct { name string metricname string expectedname string labels []string values []string countval float64 gaugeval float64 histoval float64 }{ { name: "simpleMetrics", metricname: "simple", expectedname: "simple", countval: 1, gaugeval: 2, histoval: (time.Millisecond * 5).Seconds(), }, { name: "routeMetrics", metricname: "route", labels: []string{"service", "host", "path", "target"}, values: []string{"service", "foo", "host", "bar", "path", "/asdf", "target", "http://jkl.org:1234"}, expectedname: "foo.bar./asdf.jkl_org_1234", countval: 20, gaugeval: 30, histoval: (time.Millisecond * 50).Seconds(), }, { name: "codeMetrics", metricname: "status", labels: []string{"code"}, values: []string{"code", "200"}, expectedname: "status.{type}.code.200", countval: 60, gaugeval: 70, histoval: (time.Millisecond * 80).Seconds(), }, } { t.Run(tst.name, func(t *testing.T) { cname := tst.metricname + ".count" gname := tst.metricname + ".gauge" hname := tst.metricname + ".histo" counter := provider.NewCounter(cname, tst.labels...) gauge := provider.NewGauge(gname, tst.labels...) histo := provider.NewHistogram(hname, tst.labels...) if len(tst.labels) > 0 { counter = counter.With(tst.values...) gauge = gauge.With(tst.values...) histo = histo.With(tst.values...) } counter.Add(tst.countval) gauge.Set(tst.gaugeval) histo.Observe(tst.histoval) var buff bytes.Buffer _, _ = provider.S.WriteTo(&buff) m := parseStatsdMetrics(&buff, prefix) // t.Logf("parsed metrics: %#v", m) for _, v := range []struct { n string v float64 }{ {"count", tst.countval}, {"gauge", tst.gaugeval}, {"histo", tst.histoval * 1000.0}, } { var name string // have to do this little dance because route metrics // follow a special rule if strings.Contains(tst.expectedname, "{type}") { name = strings.ReplaceAll(tst.expectedname, "{type}", v.n) } else { name = tst.expectedname + "." + v.n } if se, ok := m[name]; ok { if se.value != v.v { t.Errorf("%s failed: expected: %.02f, got: %02f", name, v.v, se.value) } } else { t.Errorf("%s not found", v.n) } } }) } } type statsdEntry struct { value float64 t string sample float64 tags []string } var re = regexp.MustCompile(`^([^:]+):([0-9\.]+)\|(ms|c|g|h)(?:\|@([0-9\.]+))?(?:\|#(.*))?$`) func parseStatsdMetrics(data io.Reader, prefix string) map[string]statsdEntry { reader := bufio.NewScanner(data) m := make(map[string]statsdEntry) for reader.Scan() { line := reader.Text() matches := re.FindStringSubmatch(line) if matches == nil { panic(line) } name := strings.TrimPrefix(matches[1], prefix) value, err := strconv.ParseFloat(matches[2], 64) if err != nil { panic(err.Error()) } var sample float64 if len(matches[4]) > 0 { sample, err = strconv.ParseFloat(matches[4], 64) if err != nil { panic(err.Error) } } var tags []string if len(matches[5]) > 0 { kvs := strings.Split(matches[5], ",") for _, kv := range kvs { tags = append(tags, strings.SplitN(kv, ":", 2)...) } } m[name] = statsdEntry{ value: value, t: matches[3], sample: sample, tags: tags, } } return m } ================================================ FILE: noroute/store.go ================================================ package noroute import ( "sync/atomic" ) var store atomic.Value // string func init() { store.Store("") } // GetHTML returns the HTML for not found routes. func GetHTML() string { return store.Load().(string) } // SetHTML sets the HTML for not found routes. func SetHTML(h string) { store.Store(h) } ================================================ FILE: noroute/store_test.go ================================================ package noroute import ( "testing" ) func TestStoreSetGet(t *testing.T) { if got, want := GetHTML(), ""; got != want { t.Fatalf("got unset noroute html %q want %q", got, want) } SetHTML("foo") if got, want := GetHTML(), "foo"; got != want { t.Fatalf("got noroute html %q want %q", got, want) } } ================================================ FILE: proxy/grpc_handler.go ================================================ package proxy import ( "context" "crypto/tls" "fmt" "log" "net" "net/http" "net/url" "sync" "time" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/route" gkm "github.com/go-kit/kit/metrics" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/grpc/stats" "google.golang.org/grpc/status" ) type gRPCServer struct { server *grpc.Server } func (s *gRPCServer) Close() error { s.server.Stop() return nil } func (s *gRPCServer) Shutdown(ctx context.Context) error { s.server.GracefulStop() return nil } func (s *gRPCServer) Serve(lis net.Listener) error { return s.server.Serve(lis) } func GetGRPCDirector(tlscfg *tls.Config, cfg *config.Config) func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) { connectionPool := newGrpcConnectionPool(tlscfg, cfg) return func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return ctx, nil, fmt.Errorf("error extracting metadata from request") } outCtx := metadata.NewOutgoingContext(ctx, md.Copy()) target, _ := ctx.Value(targetKey{}).(*route.Target) if target == nil { log.Println("[WARN] grpc: no route for ", fullMethodName) return outCtx, nil, fmt.Errorf("no route found") } conn, err := connectionPool.Get(outCtx, target) return outCtx, conn, err } } type GrpcProxyInterceptor struct { Config *config.Config StatsHandler *GrpcStatsHandler GlobCache *route.GlobCache } type targetKey struct{} type proxyStream struct { grpc.ServerStream ctx context.Context } func (p proxyStream) Context() context.Context { return p.ctx } func makeGRPCTargetKey(t *route.Target) string { return t.URL.String() } func (g GrpcProxyInterceptor) Stream(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := stream.Context() target, err := g.lookup(ctx, info.FullMethod) if err != nil { log.Println("[ERROR] grpc: error looking up route", err) return status.Error(codes.Internal, "internal error") } if target == nil { g.StatsHandler.NoRoute.Add(1) log.Println("[WARN] grpc: no route found for", info.FullMethod) return status.Error(codes.NotFound, "no route found") } ctx = context.WithValue(ctx, targetKey{}, target) proxyStream := proxyStream{ ServerStream: stream, ctx: ctx, } start := time.Now() err = handler(srv, proxyStream) end := time.Now() dur := end.Sub(start) target.Timer.Observe(dur.Seconds()) return err } func (g GrpcProxyInterceptor) lookup(ctx context.Context, fullMethodName string) (*route.Target, error) { pick := route.Picker[g.Config.Proxy.Strategy] match := route.Matcher[g.Config.Proxy.Matcher] md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, fmt.Errorf("error extracting metadata from request") } reqUrl, err := url.ParseRequestURI(fullMethodName) if err != nil { log.Print("[WARN] Error parsing grpc request url ", fullMethodName) return nil, fmt.Errorf("error parsing request url") } headers := http.Header{} for k, v := range md { for _, h := range v { headers.Add(k, h) } } //grpc client can specify a destination host in metadata dstHostSpecifiedByGRPCClient := g.getDestinationHostFromMetadata(md) //todo: better a configuration flag is required to disable/enable this function, and make it disabled by default configuration req := &http.Request{ Host: dstHostSpecifiedByGRPCClient, URL: reqUrl, Header: headers, } return route.GetTable().Lookup(req, pick, match, g.GlobCache, g.Config.GlobMatchingDisabled), nil } // grpc client can specify a destination host in metadata by key 'dsthost', e.g. dsthost=betatest // the backend service(s) tags should be urlprefix-betatest/grpcpackage.servicename proto=grpc // the 'betatest' will be parsed as 'host' and '/grpcpackage.servicename' is the 'path', // a route record will be setup in route Table, t['betatest'] // the dstHost is extracted from context's metadata of grpc client, that will trigger t[dstHost] is used. // if t[dstHost] not exists, fallback to t[""] is used // dstHost will be "" as before if not specified by grpc client side. func (g GrpcProxyInterceptor) getDestinationHostFromMetadata(md metadata.MD) (dstHost string) { dstHost = "" hosts := md["dsthost"] if len(hosts) == 1 { dstHost = hosts[0] } return } type GrpcStatsHandler struct { Connect gkm.Counter Request gkm.Histogram NoRoute gkm.Counter Status gkm.Histogram } type connCtxKey struct{} type rpcCtxKey struct{} func (h *GrpcStatsHandler) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context { return context.WithValue(ctx, connCtxKey{}, info) } func (h *GrpcStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { return context.WithValue(ctx, rpcCtxKey{}, info) } func (h *GrpcStatsHandler) HandleRPC(ctx context.Context, rpc stats.RPCStats) { rpcStats, _ := rpc.(*stats.End) if rpcStats == nil { return } dur := rpcStats.EndTime.Sub(rpcStats.BeginTime) h.Request.Observe(dur.Seconds()) s, _ := status.FromError(rpcStats.Error) h.Status.With("code", s.Code().String()).Observe(dur.Seconds()) } // HandleConn processes the Conn stats. func (h *GrpcStatsHandler) HandleConn(ctx context.Context, conn stats.ConnStats) { connBegin, _ := conn.(*stats.ConnBegin) if connBegin != nil { h.Connect.Add(1) } } type grpcConnectionPool struct { connections map[string]*grpc.ClientConn tlscfg *tls.Config cfg *config.Config cleanupInterval time.Duration lock sync.RWMutex } func newGrpcConnectionPool(tlscfg *tls.Config, cfg *config.Config) *grpcConnectionPool { cp := &grpcConnectionPool{ connections: make(map[string]*grpc.ClientConn), lock: sync.RWMutex{}, cleanupInterval: time.Second * 5, tlscfg: tlscfg, cfg: cfg, } go cp.cleanup() return cp } func (p *grpcConnectionPool) Get(ctx context.Context, target *route.Target) (*grpc.ClientConn, error) { p.lock.RLock() conn := p.connections[makeGRPCTargetKey(target)] p.lock.RUnlock() if conn != nil && conn.GetState() != connectivity.Shutdown { return conn, nil } return p.newConnection(target) } func (p *grpcConnectionPool) newConnection(target *route.Target) (*grpc.ClientConn, error) { opts := []grpc.DialOption{ grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(p.cfg.Proxy.GRPCMaxRxMsgSize)), } if target.URL.Scheme == "grpcs" && p.tlscfg != nil { opts = append(opts, grpc.WithTransportCredentials( credentials.NewTLS(&tls.Config{ ClientCAs: p.tlscfg.ClientCAs, InsecureSkipVerify: target.TLSSkipVerify, // as per the http/2 spec, the host header isn't required, so if your // target service doesn't have IP SANs in it's certificate // then you will need to override the servername ServerName: target.Opts["grpcservername"], }))) } else { opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) } conn, err := grpc.NewClient(target.URL.Host, opts...) if err == nil { p.Set(target, conn) } return conn, err } func (p *grpcConnectionPool) Set(target *route.Target, conn *grpc.ClientConn) { p.lock.Lock() defer p.lock.Unlock() p.connections[makeGRPCTargetKey(target)] = conn } func (p *grpcConnectionPool) cleanup() { for { p.lock.Lock() table := route.GetTable() for tKey, cs := range p.connections { state := cs.GetState() if state == connectivity.Shutdown { delete(p.connections, tKey) continue } if !hasTarget(tKey, table) { log.Println("[DEBUG] grpc: cleaning up connection to", tKey) go func(cs *grpc.ClientConn, state connectivity.State) { ctx, cancel := context.WithTimeout(context.Background(), p.cfg.Proxy.GRPCGShutdownTimeout) defer cancel() // wait for state to change, or timeout, before closing, in case it's still handling traffic. cs.WaitForStateChange(ctx, state) cs.Close() }(cs, state) delete(p.connections, tKey) } } p.lock.Unlock() time.Sleep(p.cleanupInterval) } } func hasTarget(tKey string, table route.Table) bool { for _, routes := range table { for _, r := range routes { for _, t := range r.Targets { if tKey == makeGRPCTargetKey(t) { return true } } } } return false } ================================================ FILE: proxy/gzip/content_type_test.go ================================================ package gzip import "testing" // TestContentTypes tests the content-type regexp that is used as // an example in fabio.properties func TestContentTypes(t *testing.T) { tests := []string{ "text/foo", "text/foo; charset=UTF-8", "text/plain", "text/plain; charset=UTF-8", "application/json", "application/json; charset=UTF-8", "application/javascript", "application/javascript; charset=UTF-8", "application/font-woff", "application/font-woff; charset=UTF-8", "application/xml", "application/xml; charset=UTF-8", "vendor/vendor.foo+json", "vendor/vendor.foo+json; charset=UTF-8", "vendor/vendor.foo+xml", "vendor/vendor.foo+xml; charset=UTF-8", } for _, tt := range tests { tt := tt // capture loop var t.Run(tt, func(t *testing.T) { if !contentTypes.MatchString(tt) { t.Fatalf("%q does not match content types regexp", tt) } }) } } ================================================ FILE: proxy/gzip/gzip_handler.go ================================================ // Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed // Package gzip provides an HTTP handler which compresses responses // if the client supports this, the response is compressable and // not already compressed. // // Based on https://github.com/smancke/handler/gzip package gzip import ( "bufio" "compress/gzip" "errors" "io" "net" "net/http" "regexp" "strings" "sync" ) const ( headerVary = "Vary" headerAccept = "Accept" headerAcceptEncoding = "Accept-Encoding" headerContentEncoding = "Content-Encoding" headerContentType = "Content-Type" headerContentLength = "Content-Length" encodingGzip = "gzip" ) var blacklistedAcceptContentTypes = []string{"text/event-stream"} var gzipWriterPool = sync.Pool{ New: func() interface{} { return gzip.NewWriter(nil) }, } // NewGzipHandler wraps an existing handler to transparently gzip the response // body if the client supports it (via the Accept-Encoding header) and the // response Content-Type matches the contentTypes expression. func NewGzipHandler(h http.Handler, contentTypes *regexp.Regexp) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add(headerVary, headerAcceptEncoding) if acceptsGzip(r) { gzWriter := NewGzipResponseWriter(w, contentTypes) defer gzWriter.Close() h.ServeHTTP(gzWriter, r) } else { h.ServeHTTP(w, r) } }) } type GzipResponseWriter struct { writer io.Writer gzipWriter *gzip.Writer contentTypes *regexp.Regexp http.ResponseWriter } func NewGzipResponseWriter(w http.ResponseWriter, contentTypes *regexp.Regexp) *GzipResponseWriter { return &GzipResponseWriter{ResponseWriter: w, contentTypes: contentTypes} } func (grw *GzipResponseWriter) WriteHeader(code int) { if grw.writer == nil { if isCompressable(grw.Header(), grw.contentTypes) { grw.Header().Del(headerContentLength) grw.Header().Set(headerContentEncoding, encodingGzip) grw.gzipWriter = gzipWriterPool.Get().(*gzip.Writer) grw.gzipWriter.Reset(grw.ResponseWriter) grw.writer = grw.gzipWriter } else { grw.writer = grw.ResponseWriter } } grw.ResponseWriter.WriteHeader(code) } func (grw *GzipResponseWriter) Write(b []byte) (int, error) { if grw.writer == nil { if _, ok := grw.Header()[headerContentType]; !ok { // Set content-type if not present. Otherwise golang would make application/gzip out of that. grw.Header().Set(headerContentType, http.DetectContentType(b)) } grw.WriteHeader(http.StatusOK) } return grw.writer.Write(b) } func (grw *GzipResponseWriter) Close() { if grw.gzipWriter != nil { grw.gzipWriter.Close() gzipWriterPool.Put(grw.gzipWriter) } } func (grw *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := grw.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, errors.New("not a Hijacker") } func isCompressable(header http.Header, contentTypes *regexp.Regexp) bool { // don't compress if it is already encoded if header.Get(headerContentEncoding) != "" { return false } return contentTypes.MatchString(header.Get(headerContentType)) } func acceptsGzip(r *http.Request) bool { accept := r.Header.Get(headerAccept) for _, contentType := range blacklistedAcceptContentTypes { if strings.Contains(accept, contentType) { return false } } return strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) } ================================================ FILE: proxy/gzip/gzip_handler_test.go ================================================ // Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed package gzip import ( "bytes" "compress/gzip" "io" "net/http" "net/http/httptest" "regexp" "strconv" "testing" "github.com/fabiolb/fabio/assert" ) var contentTypes = regexp.MustCompile(`^(text/.*|application/(javascript|json|font-woff|xml)|.*\+(json|xml))(;.*)?$`) func Test_GzipHandler_CompressableType(t *testing.T) { server := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes)) assertEqual := assert.Equal(t) r, err := http.NewRequest("GET", server.URL, nil) assertEqual(err, nil) r.Header.Set("Accept-Encoding", "gzip") resp, err := http.DefaultClient.Do(r) assertEqual(err, nil) assertEqual(resp.Header.Get("Content-Type"), "text/plain; charset=utf-8") assertEqual(resp.Header.Get("Content-Encoding"), "gzip") gzBytes, err := io.ReadAll(resp.Body) assertEqual(err, nil) assertEqual(resp.Header.Get("Content-Length"), strconv.Itoa(len(gzBytes))) reader, err := gzip.NewReader(bytes.NewBuffer(gzBytes)) assertEqual(err, nil) defer reader.Close() bytes, err := io.ReadAll(reader) assertEqual(err, nil) assertEqual(string(bytes), "Hello World") } func Test_GzipHandler_NotCompressingTwice(t *testing.T) { server := httptest.NewServer(NewGzipHandler(test_already_compressed_handler(), contentTypes)) assertEqual := assert.Equal(t) r, err := http.NewRequest("GET", server.URL, nil) assertEqual(err, nil) r.Header.Set("Accept-Encoding", "gzip") resp, err := http.DefaultClient.Do(r) assertEqual(err, nil) assertEqual(resp.Header.Get("Content-Encoding"), "gzip") reader, err := gzip.NewReader(resp.Body) assertEqual(err, nil) defer reader.Close() bytes, err := io.ReadAll(reader) assertEqual(err, nil) assertEqual(string(bytes), "Hello World") } func Test_GzipHandler_CompressableType_NoAccept(t *testing.T) { server := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes)) assertEqual := assert.Equal(t) r, err := http.NewRequest("GET", server.URL, nil) assertEqual(err, nil) r.Header.Set("Accept-Encoding", "none") resp, err := http.DefaultClient.Do(r) assertEqual(err, nil) assertEqual(resp.Header.Get("Content-Encoding"), "") bytes, err := io.ReadAll(resp.Body) assertEqual(err, nil) assertEqual(string(bytes), "Hello World") } func Test_GzipHandler_NonCompressableType(t *testing.T) { server := httptest.NewServer(NewGzipHandler(test_binary_handler(), contentTypes)) assertEqual := assert.Equal(t) r, err := http.NewRequest("GET", server.URL, nil) assertEqual(err, nil) r.Header.Set("Accept-Encoding", "gzip") resp, err := http.DefaultClient.Do(r) assertEqual(err, nil) assertEqual(resp.Header.Get("Content-Encoding"), "") bytes, err := io.ReadAll(resp.Body) assertEqual(err, nil) assertEqual(bytes, []byte{42}) } func test_text_handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b := []byte("Hello World") w.Header().Set("Content-Length", strconv.Itoa(len(b))) w.Write(b) }) } func test_binary_handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/jpg") w.Write([]byte{42}) }) } func test_already_compressed_handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") gzWriter := gzip.NewWriter(w) gzWriter.Write([]byte("Hello World")) gzWriter.Close() }) } ================================================ FILE: proxy/http_handler.go ================================================ package proxy import ( "context" "io" "log" "net" "net/http" "net/http/httputil" "net/url" "time" ) // StatusClientClosedRequest non-standard HTTP status code for client disconnection const StatusClientClosedRequest = 499 func newHTTPProxy(target *url.URL, tr http.RoundTripper, flush time.Duration) http.Handler { return &httputil.ReverseProxy{ // this is a simplified director function based on the // httputil.NewSingleHostReverseProxy() which does not // mangle the request and target URL since the target // URL is already in the correct format. Director: func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.URL.Path = target.Path req.URL.RawQuery = target.RawQuery if _, ok := req.Header["User-Agent"]; !ok { // explicitly disable User-Agent so it's not set to default value req.Header.Set("User-Agent", "") } }, FlushInterval: flush, Transport: tr, ErrorHandler: httpProxyErrorHandler, } } func httpProxyErrorHandler(w http.ResponseWriter, r *http.Request, err error) { // 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 // If a "context canceled" error is returned by the http.Request handler this means the client closed the connection before getting a response // So we are changing the StatusCode on these situations to the non-standard 499 (Client Closed Request) statusCode := http.StatusInternalServerError if e, ok := err.(net.Error); ok { if e.Timeout() { statusCode = http.StatusGatewayTimeout } else { statusCode = http.StatusBadGateway } } else if err == io.EOF { statusCode = http.StatusBadGateway } else if err == context.Canceled { statusCode = StatusClientClosedRequest } w.WriteHeader(statusCode) // 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 // Note: The access_log will still log the 499 response status codes if statusCode != StatusClientClosedRequest { log.Print("[ERROR] ", err) } } ================================================ FILE: proxy/http_headers.go ================================================ package proxy import ( "crypto/tls" "errors" "net" "net/http" "net/textproto" "strings" "github.com/fabiolb/fabio/config" ) // addResponseHeaders adds/updates headers in the response func addResponseHeaders(w http.ResponseWriter, r *http.Request, cfg config.Proxy) { if r.TLS != nil && cfg.STSHeader.MaxAge > 0 { sts := "max-age=" + i32toa(int32(cfg.STSHeader.MaxAge)) if cfg.STSHeader.Subdomains { sts += "; includeSubdomains" } if cfg.STSHeader.Preload { sts += "; preload" } w.Header().Set("Strict-Transport-Security", sts) } } var protectHeaders = map[string]bool{ "Forwarded": true, "X-Forwarded-For": true, "X-Forwarded-Host": true, "X-Forwarded-Port": true, "X-Forwarded-Proto": true, "X-Forwarded-Prefix": true, "X-Real-Ip": true, } // addHeaders adds/updates headers in request // // * add/update `Forwarded` header // * add X-Forwarded-Proto header, if not present // * add X-Real-Ip, if not present // * ClientIPHeader != "": Set header with that name to // * TLS connection: Set header with name from `cfg.TLSHeader` to `cfg.TLSHeaderValue` func addHeaders(r *http.Request, cfg config.Proxy, stripPath string) error { remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return errors.New("cannot parse " + r.RemoteAddr) } // exclude headers from Connection rules. var conHeaders []string for _, s := range r.Header.Values("Connection") { for _, p := range strings.Split(s, ",") { p = strings.TrimSpace(p) if !protectHeaders[textproto.CanonicalMIMEHeaderKey(p)] { conHeaders = append(conHeaders, p) } } } r.Header.Del("Connection") if len(conHeaders) > 0 { r.Header.Set("Connection", strings.Join(conHeaders, ", ")) } // set configurable ClientIPHeader // X-Real-Ip is set later and X-Forwarded-For is set // by the Go HTTP reverse proxy. if cfg.ClientIPHeader != "" && cfg.ClientIPHeader != "X-Forwarded-For" && cfg.ClientIPHeader != "X-Real-Ip" { r.Header.Set(cfg.ClientIPHeader, remoteIP) } if r.Header.Get("X-Real-Ip") == "" { r.Header.Set("X-Real-Ip", remoteIP) } // set the X-Forwarded-For header for websocket // connections since they aren't handled by the // http proxy which sets it. ws := r.Header.Get("Upgrade") == "websocket" if ws { clientIP := remoteIP // If we aren't the first proxy retain prior // X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. prior, ok := r.Header["X-Forwarded-For"] omit := ok && prior == nil // Issue 38079: nil now means don't populate the header if len(prior) > 0 { clientIP = strings.Join(prior, ", ") + ", " + clientIP } if !omit { r.Header.Set("X-Forwarded-For", clientIP) } } // Issue #133: Setting the X-Forwarded-Proto header to // anything other than 'http' or 'https' breaks java // websocket clients which use java.net.URL for composing // the forwarded URL. Since X-Forwarded-Proto is not // specified the common practice is to set it to either // 'http' for 'ws' and 'https' for 'wss' connections. proto := scheme(r) if r.Header.Get("X-Forwarded-Proto") == "" { switch proto { case "ws": r.Header.Set("X-Forwarded-Proto", "http") case "wss": r.Header.Set("X-Forwarded-Proto", "https") default: r.Header.Set("X-Forwarded-Proto", proto) } } if r.Header.Get("X-Forwarded-Port") == "" { r.Header.Set("X-Forwarded-Port", localPort(r)) } if r.Header.Get("X-Forwarded-Host") == "" && r.Host != "" { r.Header.Set("X-Forwarded-Host", r.Host) } if stripPath != "" { r.Header.Set("X-Forwarded-Prefix", stripPath) } fwd := r.Header.Get("Forwarded") if fwd == "" { fwd = "for=" + remoteIP + "; proto=" + proto } if cfg.LocalIP != "" { fwd += "; by=" + cfg.LocalIP } if r.Proto != "" { fwd += "; httpproto=" + strings.ToLower(r.Proto) } if r.TLS != nil && r.TLS.Version > 0 { v := tlsver[r.TLS.Version] if v == "" { v = uint16base16(r.TLS.Version) } fwd += "; tlsver=" + v } if r.TLS != nil && r.TLS.CipherSuite != 0 { fwd += "; tlscipher=" + uint16base16(r.TLS.CipherSuite) } r.Header.Set("Forwarded", fwd) if cfg.TLSHeader != "" { if r.TLS != nil { r.Header.Set(cfg.TLSHeader, cfg.TLSHeaderValue) } else { r.Header.Del(cfg.TLSHeader) } } return nil } var tlsver = map[uint16]string{ tls.VersionTLS10: "tls10", tls.VersionTLS11: "tls11", tls.VersionTLS12: "tls12", } var digit16 = []byte("0123456789abcdef") // uint16base64 is a faster version of fmt.Sprintf("0x%04x", n) // // BenchmarkUint16Base16/fmt.Sprintf-8 10000000 154 ns/op 8 B/op 2 allocs/op // BenchmarkUint16Base16/uint16base16-8 50000000 35.0 ns/op 8 B/op 1 allocs/op func uint16base16(n uint16) string { b := []byte("0x0000") b[5] = digit16[n&0x000f] b[4] = digit16[n&0x00f0>>4] b[3] = digit16[n&0x0f00>>8] b[2] = digit16[n&0xf000>>12] return string(b) } // i32toa is a faster implementation of strconv.Itoa() without importing another library // https://stackoverflow.com/a/39444005 func i32toa(n int32) string { buf := [11]byte{} pos := len(buf) i := int64(n) signed := i < 0 if signed { i = -i } for { pos-- buf[pos], i = '0'+byte(i%10), i/10 if i == 0 { if signed { pos-- buf[pos] = '-' } return string(buf[pos:]) } } } // scheme derives the request scheme used on the initial // request first from headers and then from the connection // using the following heuristic: // // If either X-Forwarded-Proto or Forwarded is set then use // its value to set the other header. If both headers are // set do not modify the protocol. If none are set derive // the protocol from the connection. func scheme(r *http.Request) string { xfp := r.Header.Get("X-Forwarded-Proto") fwd := r.Header.Get("Forwarded") switch { case xfp != "" && fwd == "": return xfp case fwd != "" && xfp == "": p := strings.SplitAfterN(fwd, "proto=", 2) if len(p) == 1 { break } n := strings.IndexRune(p[1], ';') if n >= 0 { return p[1][:n] } return p[1] } ws := r.Header.Get("Upgrade") == "websocket" switch { case ws && r.TLS != nil: return "wss" case ws && r.TLS == nil: return "ws" case r.TLS != nil: return "https" default: return "http" } } func localPort(r *http.Request) string { if r == nil { return "" } n := strings.Index(r.Host, ":") if n > 0 && n < len(r.Host)-1 { return r.Host[n+1:] } if r.TLS != nil { return "443" } return "80" } ================================================ FILE: proxy/http_headers_test.go ================================================ package proxy import ( "crypto/tls" "fmt" "net/http" "net/http/httptest" "testing" "github.com/fabiolb/fabio/config" "github.com/pascaldekloe/goe/verify" ) func TestAddHeaders(t *testing.T) { tests := []struct { desc string r *http.Request cfg config.Proxy strip string hdrs http.Header err string }{ {"error", &http.Request{RemoteAddr: "1.2.3.4"}, config.Proxy{}, "", http.Header{}, "cannot parse 1.2.3.4", }, {"http request", &http.Request{RemoteAddr: "1.2.3.4:5555"}, config.Proxy{}, "/foo", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Forwarded-Prefix": []string{"/foo"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"https request", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"https request hack", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Connection": {"keep-alive", "X-Real-Ip"}}}, config.Proxy{}, "", http.Header{ "Connection": []string{"keep-alive"}, "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"ws request", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Upgrade": {"websocket"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=ws"}, "Upgrade": []string{"websocket"}, "X-Forwarded-For": []string{"1.2.3.4"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"wss request", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Upgrade": {"websocket"}}, TLS: &tls.ConnectionState{}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=wss"}, "Upgrade": []string{"websocket"}, "X-Forwarded-For": []string{"1.2.3.4"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set client ip header", &http.Request{RemoteAddr: "1.2.3.4:5555"}, config.Proxy{ClientIPHeader: "Client-IP"}, "", http.Header{ "Client-Ip": []string{"1.2.3.4"}, "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set Forwarded with localIP", &http.Request{RemoteAddr: "1.2.3.4:5555"}, config.Proxy{LocalIP: "5.6.7.8"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http; by=5.6.7.8"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set Forwarded with localIP for https", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{LocalIP: "5.6.7.8"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https; by=5.6.7.8"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set httpproto, tlsver and tlscipher on Forwarded for https", &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}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https; httpproto=http/1.1; tlsver=tls10; tlscipher=0xc023"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set httpproto on Forwarded", &http.Request{RemoteAddr: "1.2.3.4:5555", Proto: "HTTP/1.1"}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http; httpproto=http/1.1"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"extend Forwarded with localIP", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Forwarded": {"for=9.9.9.9; proto=http; by=8.8.8.8"}}}, config.Proxy{LocalIP: "5.6.7.8"}, "", http.Header{ "Forwarded": []string{"for=9.9.9.9; proto=http; by=8.8.8.8; by=5.6.7.8"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set tls header", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{TLSHeader: "Secure"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https"}, "Secure": []string{""}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set tls header with value", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{TLSHeader: "Secure", TLSHeaderValue: "true"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https"}, "Secure": []string{"true"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"overwrite tls header for https, when set", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Secure": []string{"on"}}, TLS: &tls.ConnectionState{}}, config.Proxy{TLSHeader: "Secure", TLSHeaderValue: "true"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https"}, "Secure": []string{"true"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Port": []string{"443"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"drop tls header for http, when set", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Secure": []string{"on"}}}, config.Proxy{TLSHeader: "Secure", TLSHeaderValue: "true"}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"do not overwrite X-Forwarded-Proto, if present", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"X-Forwarded-Proto": {"some value"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=some value"}, "X-Forwarded-Proto": []string{"some value"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set scheme from X-Forwarded-Proto, if present and Forwarded is missing", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"X-Forwarded-Proto": {"some value"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=some value"}, "X-Forwarded-Proto": []string{"some value"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set scheme from Forwarded, if present and X-Forwarded-Proto is missing", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"Forwarded": {"for=1.2.3.4; proto=some value"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=some value"}, "X-Forwarded-Proto": []string{"some value"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"do not modify scheme when both Forwarded and X-Forwarded-Proto are present", &http.Request{ RemoteAddr: "1.2.3.4:5555", Header: http.Header{ "Forwarded": {"for=1.2.3.4; proto=some value"}, "X-Forwarded-Proto": {"other value"}, }, }, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=some value"}, "X-Forwarded-Proto": []string{"other value"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set X-Forwarded-Port from Host", &http.Request{RemoteAddr: "1.2.3.4:5555", Host: "5.6.7.8:1234"}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Host": []string{"5.6.7.8:1234"}, "X-Forwarded-Port": []string{"1234"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set X-Forwarded-Port from Host for https", &http.Request{RemoteAddr: "1.2.3.4:5555", Host: "5.6.7.8:1234", TLS: &tls.ConnectionState{}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=https"}, "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"5.6.7.8:1234"}, "X-Forwarded-Port": []string{"1234"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"do not overwrite X-Forwarded-Port header, if present", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"X-Forwarded-Port": {"4444"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"4444"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"set X-Forwarded-Host from Host", &http.Request{RemoteAddr: "1.2.3.4:5555", Host: "5.6.7.8:1234"}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Host": []string{"5.6.7.8:1234"}, "X-Forwarded-Port": []string{"1234"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"do not overwrite X-Forwarded-Host, if present", &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"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Host": []string{"9.10.11.12:1234"}, "X-Forwarded-Port": []string{"1234"}, "X-Real-Ip": []string{"1.2.3.4"}, }, "", }, {"do not overwrite X-Real-Ip, if present", &http.Request{RemoteAddr: "1.2.3.4:5555", Header: http.Header{"X-Real-Ip": {"6.6.6.6"}}}, config.Proxy{}, "", http.Header{ "Forwarded": []string{"for=1.2.3.4; proto=http"}, "X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Port": []string{"80"}, "X-Real-Ip": []string{"6.6.6.6"}, }, "", }, } for i, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { if tt.r.Header == nil { tt.r.Header = http.Header{} } err := addHeaders(tt.r, tt.cfg, tt.strip) if err != nil { if got, want := err.Error(), tt.err; got != want { t.Fatalf("%d: %s\ngot %q\nwant %q", i, tt.desc, got, want) } return } if tt.err != "" { t.Fatalf("%d: got nil want %q", i, tt.err) return } got, want := tt.r.Header, tt.hdrs verify.Values(t, "", got, want) }) } } func TestAddResponseHeaders(t *testing.T) { tests := []struct { desc string r *http.Request cfg config.Proxy hdrs http.Header err string }{ {"set Strict-Transport-Security for TLS, if MaxAge greater than 0", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}}, http.Header{ "Strict-Transport-Security": []string{"max-age=31536000"}, }, "", }, {"set Strict-Transport-Security for TLS, if MaxAge greater than 0 with options", &http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}}, config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000, Preload: true, Subdomains: true}}, http.Header{ "Strict-Transport-Security": []string{"max-age=31536000; includeSubdomains; preload"}, }, "", }, {"skip Strict-Transport-Security for non-TLS, if MaxAge greater than 0", &http.Request{RemoteAddr: "1.2.3.4:5555"}, config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}}, http.Header{}, "", }, } for i, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { if tt.r.Header == nil { tt.r.Header = http.Header{} } w := httptest.NewRecorder() addResponseHeaders(w, tt.r, tt.cfg) if tt.err != "" { t.Fatalf("%d: got nil want %q", i, tt.err) return } resp := w.Result() got, want := resp.Header, tt.hdrs verify.Values(t, "", got, want) }) } } func TestLocalPort(t *testing.T) { tests := []struct { r *http.Request port string }{ {nil, ""}, {&http.Request{Host: ""}, "80"}, {&http.Request{Host: ":"}, "80"}, {&http.Request{Host: "1.2.3.4:5678"}, "5678"}, {&http.Request{Host: "1.2.3.4"}, "80"}, {&http.Request{Host: "1.2.3.4", TLS: &tls.ConnectionState{}}, "443"}, {&http.Request{Host: "1.2.3.4:"}, "80"}, {&http.Request{Host: "1.2.3.4:", TLS: &tls.ConnectionState{}}, "443"}, } for i, tt := range tests { if got, want := localPort(tt.r), tt.port; got != want { t.Errorf("%d: got %q want %q", i, got, want) } } } func TestUint16Base16(t *testing.T) { for i := range uint16(9999) { if got, want := uint16base16(i), fmt.Sprintf("0x%04x", i); got != want { t.Fatalf("got %q for %04x want %q", got, i, want) } } } func BenchmarkUint16Base16(b *testing.B) { // keep a variable outside of the tests so that the compiler doesn't // optimize the body of the loop away. var s string b.Run("fmt.Sprintf", func(b *testing.B) { for i := range b.N { s = fmt.Sprintf("0x%04x", uint16(i)) } }) b.Run("uint16base16", func(b *testing.B) { for i := range b.N { s = uint16base16(uint16(i)) } }) b.Logf("BenchmarkUint16Base16 %v", s) // use the var to make go1.10 vet happy } ================================================ FILE: proxy/http_integration_test.go ================================================ package proxy import ( "bytes" "compress/gzip" "crypto/tls" "crypto/x509" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "os" "regexp" "sort" "strconv" "strings" "testing" "time" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy/internal" "github.com/fabiolb/fabio/route" "github.com/pascaldekloe/goe/verify" ) const ( // helper constants for the Lookup function globEnabled = false globDisabled = true ) // Global GlobCache for Testing var globCache = route.NewGlobCache(1000) const ( legitHeader1 = "Legit-Header1" legitHeader2 = "Legit-Header2" ) func TestProxyProducesCorrectXForwardedSomethingHeader(t *testing.T) { var hdr = make(http.Header) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hdr = r.Header })) defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{LocalIP: "1.1.1.1", ClientIPHeader: "X-Forwarded-For"}, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return &route.Target{URL: mustParse(server.URL)} }, }) defer proxy.Close() req, _ := http.NewRequest("GET", proxy.URL, nil) req.Host = "foo.com" req.Header.Set("X-Forwarded-For", "3.3.3.3") req.Header.Set(legitHeader1, "asdf") req.Header.Set(legitHeader2, "qwerty") req.Header.Set("Connection", fmt.Sprintf("keep-alive, x-forwarded-for, x-forwarded-host, %s, %s", strings.ToLower(legitHeader1), strings.ToLower(legitHeader2))) mustDo(req) if got, want := hdr.Get("X-Forwarded-For"), "3.3.3.3, 127.0.0.1"; got != want { t.Errorf("got %v want %v", got, want) } if got, want := hdr.Get("X-Forwarded-Host"), "foo.com"; got != want { t.Errorf("got %v want %v", got, want) } if got, want := hdr.Get(legitHeader1), ""; got != want { t.Errorf("got %v want %v", got, want) } if got, want := hdr.Get(legitHeader2), ""; got != want { t.Errorf("got %v want %v", got, want) } } func TestProxyRequestIDHeader(t *testing.T) { got := "not called" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got = r.Header.Get("X-Request-ID") })) defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{RequestID: "X-Request-Id"}, Transport: http.DefaultTransport, UUID: func() string { return "f47ac10b-58cc-0372-8567-0e02b2c3d479" }, Lookup: func(r *http.Request) *route.Target { return &route.Target{URL: mustParse(server.URL)} }, }) defer proxy.Close() req, _ := http.NewRequest("GET", proxy.URL, nil) mustDo(req) if want := "f47ac10b-58cc-0372-8567-0e02b2c3d479"; got != want { t.Errorf("got %v, but want %v", got, want) } } func TestProxySTSHeader(t *testing.T) { server := httptest.NewServer(okHandler) defer server.Close() proxy := httptest.NewTLSServer(&HTTPProxy{ Config: config.Proxy{ STSHeader: config.STSHeader{ MaxAge: 31536000, Subdomains: true, Preload: true, }, }, Transport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { return &route.Target{URL: mustParse(server.URL)} }, }) defer proxy.Close() client := http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsInsecureConfig(), }, } resp, err := client.Get(proxy.URL) if err != nil { panic(err) } if got, want := resp.Header.Get("Strict-Transport-Security"), "max-age=31536000; includeSubdomains; preload"; got != want { t.Errorf("got %v want %v", got, want) } } func TestProxyChecksHeaderForAccessRules(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "OK") })) defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{}, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tgt := &route.Target{ URL: mustParse(server.URL), Opts: map[string]string{"allow": "ip:127.0.0.0/8,ip:fe80::/10,ip:::1"}, } tgt.ProcessAccessRules() return tgt }, }) defer proxy.Close() req, _ := http.NewRequest("GET", proxy.URL, nil) req.Header.Set("X-Forwarded-For", "1.2.3.4") resp, _ := mustDo(req) if got, want := resp.StatusCode, http.StatusForbidden; got != want { t.Errorf("got %v want %v", got, want) } } func TestProxyNoRouteHTML(t *testing.T) { want := "503" noroute.SetHTML(want) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(*http.Request) *route.Target { return nil }, }) defer proxy.Close() _, got := mustGet(proxy.URL) if !bytes.Equal(got, []byte(want)) { t.Fatalf("got %s want %s", got, want) } } func TestProxyNoRouteStatus(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{NoRouteStatus: 999}, Transport: http.DefaultTransport, Lookup: func(*http.Request) *route.Target { return nil }, }) defer proxy.Close() resp, _ := mustGet(proxy.URL) if got, want := resp.StatusCode, 999; got != want { t.Fatalf("got %d want %d", got, want) } } func TestProxyStripsPath(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/bar": w.Write([]byte("OK")) default: w.WriteHeader(404) } })) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add mock /foo/bar " + server.URL + ` opts "strip=/foo"`)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() resp, body := mustGet(proxy.URL + "/foo/bar") if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), "OK"; got != want { t.Fatalf("got body %q want %q", got, want) } } func TestProxyPrependsPath(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/foo/bar": w.Write([]byte("OK")) default: w.WriteHeader(404) } })) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add mock /bar " + server.URL + ` opts "prepend=/foo"`)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() resp, body := mustGet(proxy.URL + "/bar") if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), "OK"; got != want { t.Fatalf("got body %q want %q", got, want) } } func TestProxyHost(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, r.Host) })) // create a static route table so that we can see the effect // of round robin distribution. The other tests generate the // route table on the fly since order does not matter to them. routes := "route add mock /hostdst http://a.com/ opts \"host=dst\"\n" routes += "route add mock /hostcustom http://a.com/ opts \"host=foo.com\"\n" routes += "route add mock /hostcustom http://b.com/ opts \"host=bar.com\"\n" routes += "route add mock / http://a.com/" tbl, _ := route.NewTable(bytes.NewBufferString(routes)) proxy := httptest.NewServer(&HTTPProxy{ Transport: &http.Transport{ Dial: func(network, _ string) (net.Conn, error) { addr := server.URL[len("http://"):] return net.Dial(network, addr) }, }, Lookup: func(r *http.Request) *route.Target { return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() check := func(t *testing.T, uri, host string) { resp, body := mustGet(proxy.URL + uri) if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), host; got != want { t.Fatalf("got body %q want %q", got, want) } } proxyHost := proxy.URL[len("http://"):] // test that for 'host=dst' the Host header is set to the hostname of the // target, in this case 'a.com' t.Run("host eq dst", func(t *testing.T) { check(t, "/hostdst", "a.com") }) // test that without a 'host' option no Host header is set t.Run("no host", func(t *testing.T) { check(t, "/", proxyHost) }) // 1. Test that a host header is set when the 'host' option is used. // // 2. Test that the host header is set per target, i.e. that different // targets can have different 'host' options. // // The proxy is configured to use "rr" (round-robin) distribution // for the requests. Therefore, requests to '/hostcustom' will be // sent to the two different targets in alternating order. t.Run("host is custom", func(t *testing.T) { check(t, "/hostcustom", "foo.com") check(t, "/hostcustom", "bar.com") }) } func TestHostRedirect(t *testing.T) { routes := "route add https-redir *:80 https://$host$path opts \"redirect=301\"\n" tbl, _ := route.NewTable(bytes.NewBufferString(routes)) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { r.Host = "c.com" return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() tests := []struct { req string wantCode int wantLoc string }{ {req: "/baz", wantCode: 301, wantLoc: "https://c.com/baz"}, } http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { // do not follow redirects return http.ErrUseLastResponse } for _, tt := range tests { resp, _ := mustGet(proxy.URL + tt.req) if resp.StatusCode != tt.wantCode { t.Errorf("got status code %d, want %d", resp.StatusCode, tt.wantCode) } gotLoc, _ := resp.Location() if gotLoc.String() != tt.wantLoc { t.Errorf("got location %s, want %s", gotLoc, tt.wantLoc) } } } func TestPathRedirect(t *testing.T) { routes := "route add mock / http://a.com/$path opts \"redirect=301\"\n" routes += "route add mock /foo http://a.com/abc opts \"redirect=301\"\n" routes += "route add mock /bar http://b.com/$path opts \"redirect=302 strip=/bar\"\n" tbl, _ := route.NewTable(bytes.NewBufferString(routes)) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() tests := []struct { req string wantCode int wantLoc string }{ {req: "/", wantCode: 301, wantLoc: "http://a.com/"}, {req: "/aaa/bbb", wantCode: 301, wantLoc: "http://a.com/aaa/bbb"}, {req: "/foo", wantCode: 301, wantLoc: "http://a.com/abc"}, {req: "/bar", wantCode: 302, wantLoc: "http://b.com/"}, {req: "/bar/aaa", wantCode: 302, wantLoc: "http://b.com/aaa"}, } http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { // do not follow redirects return http.ErrUseLastResponse } for _, tt := range tests { resp, _ := mustGet(proxy.URL + tt.req) if resp.StatusCode != tt.wantCode { t.Errorf("got status code %d, want %d", resp.StatusCode, tt.wantCode) } gotLoc, _ := resp.Location() if gotLoc.String() != tt.wantLoc { t.Errorf("got location %s, want %s", gotLoc, tt.wantLoc) } } } func TestProxyLogOutput(t *testing.T) { t.Run("uncompressed response", func(t *testing.T) { testProxyLogOutput(t, 73, config.Proxy{}) }) t.Run("compression enabled but no match", func(t *testing.T) { testProxyLogOutput(t, 73, config.Proxy{ GZIPContentTypes: regexp.MustCompile(`^$`), }) }) t.Run("compression enabled and active", func(t *testing.T) { testProxyLogOutput(t, 28, config.Proxy{ GZIPContentTypes: regexp.MustCompile(`.*`), }) }) } func testProxyLogOutput(t *testing.T, bodySize int, cfg config.Proxy) { t.Helper() // build a format string from all log fields and one header field fields := []string{"header.X-Foo:$header.X-Foo"} for _, k := range logger.Fields { fields = append(fields, k[1:]+":"+k) } format := strings.Join(fields, ";") // create a logger var b bytes.Buffer l, err := logger.New(&b, format) if err != nil { t.Fatal("logger.New: ", err) } // create an upstream server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo") })) defer server.Close() // create a proxy handler with mocked time tm := time.Date(2016, 1, 1, 0, 0, 0, 12345678, time.UTC) proxyHandler := &HTTPProxy{ Config: cfg, Time: func() time.Time { defer func() { tm = tm.Add(1111111111 * time.Nanosecond) }() return tm }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return &route.Target{ Service: "svc-a", URL: mustParse(server.URL), } }, Logger: l, } // start an http server with the proxy handler // which captures some parameters from the request var remoteAddr string proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { remoteAddr = r.RemoteAddr proxyHandler.ServeHTTP(w, r) })) defer proxy.Close() // create the request req, _ := http.NewRequest("GET", proxy.URL+"/foo?x=y", nil) req.Host = "example.com" req.Header.Set("X-Foo", "bar") // execute the request resp, _ := mustDo(req) if resp.StatusCode != http.StatusOK { t.Fatal("http.Get: want 200 got ", resp.StatusCode) } upstreamURL, _ := url.Parse(server.URL) upstreamHost, upstreamPort, _ := net.SplitHostPort(upstreamURL.Host) remoteHost, remotePort, _ := net.SplitHostPort(remoteAddr) want := []string{ "header.X-Foo:bar", "remote_addr:" + remoteAddr, "remote_host:" + remoteHost, "remote_port:" + remotePort, "request:GET /foo?x=y HTTP/1.1", "request_args:x=y", "request_host:example.com", "request_method:GET", "request_proto:HTTP/1.1", "request_scheme:http", "request_uri:/foo?x=y", "request_url:http://example.com/foo?x=y", "response_body_size:" + strconv.Itoa(bodySize), "response_status:200", "response_time_ms:1.111", "response_time_ns:1.111111111", "response_time_us:1.111111", "time_common:01/Jan/2016:00:00:01 +0000", "time_rfc3339:2016-01-01T00:00:01Z", "time_rfc3339_ms:2016-01-01T00:00:01.123Z", "time_rfc3339_ns:2016-01-01T00:00:01.123456789Z", "time_rfc3339_us:2016-01-01T00:00:01.123456Z", "time_unix_ms:1451606401123", "time_unix_ns:1451606401123456789", "time_unix_us:1451606401123456", "upstream_addr:" + upstreamURL.Host, "upstream_host:" + upstreamHost, "upstream_port:" + upstreamPort, "upstream_request_scheme:" + upstreamURL.Scheme, "upstream_request_uri:/foo?x=y", "upstream_request_url:" + upstreamURL.String() + "/foo?x=y", "upstream_service:svc-a", } data := b.String() data = data[:len(data)-1] // strip \n got := strings.Split(data, ";") sort.Strings(got) verify.Values(t, "", got, want) } func TestProxyHTTPSUpstream(t *testing.T) { server := httptest.NewUnstartedServer(okHandler) server.TLS = tlsServerConfig() server.StartTLS() defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv / " + server.URL + ` opts "proto=https"`)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() resp, body := mustGet(proxy.URL) if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), "OK"; got != want { t.Fatalf("got body %q want %q", got, want) } } type sniHandler struct { sni string } func (s *sniHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if request.TLS != nil { s.sni = request.TLS.ServerName } writer.Write([]byte(`OK`)) } func TestProxyHTTPSTransport(t *testing.T) { sni := &sniHandler{} server := httptest.NewUnstartedServer(sni) server.TLS = tlsServerConfig() server.StartTLS() defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv / " + server.URL + ` opts "proto=https host=foo.com tlsskipverify=true"`)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() resp, body := mustGet(proxy.URL) if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), "OK"; got != want { t.Fatalf("got body %q want %q", got, want) } if got, want := sni.sni, "foo.com"; got != want { t.Fatalf("got sni %q want %q", got, want) } } func TestProxyHTTPSUpstreamSkipVerify(t *testing.T) { server := httptest.NewUnstartedServer(okHandler) server.TLS = &tls.Config{} server.StartTLS() defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{}, Transport: http.DefaultTransport, InsecureTransport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv / " + server.URL + ` opts "proto=https tlsskipverify=true"`)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() resp, body := mustGet(proxy.URL) if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got status %d want %d", got, want) } if got, want := string(body), "OK"; got != want { t.Fatalf("got body %q want %q", got, want) } } func TestProxyGzipHandler(t *testing.T) { tests := []struct { desc string content http.HandlerFunc acceptEncoding string contentEncoding string wantResponse []byte }{ { desc: "plain body - compressed response", content: plainHandler("text/plain"), acceptEncoding: "gzip", contentEncoding: "gzip", wantResponse: gzipContent, }, { desc: "plain body - compressed response (with charset)", content: plainHandler("text/plain; charset=UTF-8"), acceptEncoding: "gzip", contentEncoding: "gzip", wantResponse: gzipContent, }, { desc: "compressed body - compressed response", content: gzipHandler("text/plain; charset=UTF-8"), acceptEncoding: "gzip", contentEncoding: "gzip", wantResponse: gzipContent, }, { desc: "plain body - plain response", content: plainHandler("text/plain"), acceptEncoding: "", contentEncoding: "", wantResponse: plainContent, }, { desc: "compressed body - plain response", content: gzipHandler("text/plain"), acceptEncoding: "", contentEncoding: "", wantResponse: plainContent, }, { desc: "plain body - plain response (no match)", content: plainHandler("text/javascript"), acceptEncoding: "gzip", contentEncoding: "", wantResponse: plainContent, }, } for _, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { server := httptest.NewServer(tt.content) defer server.Close() proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{ GZIPContentTypes: regexp.MustCompile("^text/plain(;.*)?$"), }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { return &route.Target{URL: mustParse(server.URL)} }, }) defer proxy.Close() req, _ := http.NewRequest("GET", proxy.URL, nil) req.Header.Set("Accept-Encoding", tt.acceptEncoding) resp, body := mustDo(req) if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got code %d want %d", got, want) } if got, want := resp.Header.Get("Content-Encoding"), tt.contentEncoding; got != want { t.Errorf("got content-encoding %q want %q", got, want) } if got, want := body, tt.wantResponse; !bytes.Equal(got, want) { t.Errorf("got body %q want %q", got, want) } }) } } var plainContent = []byte("Hello World") var gzipContent = compress(plainContent) func plainHandler(contentType string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType) w.Write(plainContent) } } func gzipHandler(contentType string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Encoding", "gzip") w.Write(gzipContent) } } var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }) func tlsInsecureConfig() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } func tlsClientConfig() *tls.Config { rootCAs := x509.NewCertPool() if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { panic("could not parse cert") } if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert2); !ok { panic("could not parse cert") } return &tls.Config{RootCAs: rootCAs} } func tlsServerConfig() *tls.Config { cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey) if err != nil { panic("failed to set cert") } return &tls.Config{Certificates: []tls.Certificate{cert}} } func tlsServerConfig2() *tls.Config { cert, err := tls.X509KeyPair(internal.LocalhostCert2, internal.LocalhostKey2) if err != nil { panic("failed to set cert") } return &tls.Config{Certificates: []tls.Certificate{cert}} } func mustParse(rawurl string) *url.URL { u, err := url.Parse(rawurl) if err != nil { panic(err) } return u } func mustDo(req *http.Request) (*http.Response, []byte) { resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } return resp, body } func mustGet(urlstr string) (*http.Response, []byte) { req, err := http.NewRequest("GET", urlstr, nil) if err != nil { panic(err) } return mustDo(req) } // compress returns the gzip compressed content of b. func compress(b []byte) []byte { var buf bytes.Buffer w := gzip.NewWriter(&buf) if _, err := w.Write(b); err != nil { panic(err) } if err := w.Close(); err != nil { panic(err) } return buf.Bytes() } func BenchmarkProxyLogger(b *testing.B) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer server.Close() format := "remote_addr time request body_bytes_sent http_referer http_user_agent server_name proxy_endpoint response_time request_args " l, err := logger.New(os.Stdout, format) if err != nil { b.Fatal("logger.NewHTTPLogger:", err) } proxy := &HTTPProxy{ Config: config.Proxy{ LocalIP: "1.1.1.1", ClientIPHeader: "X-Forwarded-For", }, Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add mock / " + server.URL)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, Logger: l, } req := &http.Request{ RequestURI: "/", Header: http.Header{"X-Forwarded-For": {"1.2.3.4"}}, RemoteAddr: "2.2.2.2:666", URL: &url.URL{}, Method: "GET", Proto: "HTTP/1.1", } for range b.N { proxy.ServeHTTP(httptest.NewRecorder(), req) } } ================================================ FILE: proxy/http_proxy.go ================================================ package proxy import ( "bufio" "crypto/tls" "errors" gkm "github.com/go-kit/kit/metrics" "io" "net" "net/http" "net/url" "strconv" "strings" "time" "github.com/fabiolb/fabio/auth" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy/gzip" "github.com/fabiolb/fabio/route" "github.com/fabiolb/fabio/uuid" ) type HttpStatsHandler struct { // Requests is a histogram metric which is updated for every request. Requests gkm.Histogram // Noroute is a counter metric which is updated for every request // where Lookup() returns nil. Noroute gkm.Counter // WSConn counts the number of open web socket connections. WSConn gkm.Gauge // StatusTimer is a histogram for given status codes StatusTimer gkm.Histogram // RedirectCounter - counts redirects RedirectCounter gkm.Counter } // HTTPProxy is a dynamic reverse proxy for HTTP and HTTPS protocols. type HTTPProxy struct { // stats contains all of the stats bits Stats HttpStatsHandler // Transport is the http connection pool configured with timeouts. // The proxy will panic if this value is nil. Transport http.RoundTripper // InsecureTransport is the http connection pool configured with // InsecureSkipVerify set. This is used for https proxies with // self-signed certs. InsecureTransport http.RoundTripper // Time returns the current time as the number of seconds since the epoch. // If Time is nil, time.Now is used. Time func() time.Time // Lookup returns a target host for the given request. // The proxy will panic if this value is nil. Lookup func(*http.Request) *route.Target // Logger is the access logger for the requests. Logger logger.Logger // UUID returns a unique id in uuid format. // If UUID is nil, uuid.NewUUID() is used. UUID func() string // Auth schemes registered with the server AuthSchemes map[string]auth.AuthScheme // Config is the proxy configuration as provided during startup. Config config.Proxy } func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if p.Lookup == nil { panic("no lookup function") } if p.Config.RequestID != "" { id := p.UUID if id == nil { id = uuid.NewUUID } r.Header.Set(p.Config.RequestID, id()) } t := p.Lookup(r) if t == nil { status := p.Config.NoRouteStatus if status < 100 || status > 999 { status = http.StatusNotFound } w.WriteHeader(status) html := noroute.GetHTML() if html != "" { io.WriteString(w, html) } return } if t.AccessDeniedHTTP(r) { http.Error(w, "access denied", http.StatusForbidden) return } if !t.Authorized(r, w, p.AuthSchemes) { http.Error(w, "authorization failed", http.StatusUnauthorized) return } // build the request url since r.URL will get modified // by the reverse proxy and contains only the RequestURI anyway requestURL := &url.URL{ Scheme: scheme(r), Host: r.Host, Path: r.URL.Path, RawQuery: r.URL.RawQuery, } if t.RedirectCode != 0 && t.RedirectURL != nil { http.Redirect(w, r, t.RedirectURL.String(), t.RedirectCode) if p.Stats.RedirectCounter != nil { p.Stats.RedirectCounter.With("code", strconv.Itoa(t.RedirectCode)).Add(1) } return } // build the real target url that is passed to the proxy targetURL := &url.URL{ Scheme: t.URL.Scheme, Host: t.URL.Host, Path: r.URL.Path, } if t.URL.RawQuery == "" || r.URL.RawQuery == "" { targetURL.RawQuery = t.URL.RawQuery + r.URL.RawQuery } else { targetURL.RawQuery = t.URL.RawQuery + "&" + r.URL.RawQuery } if t.Host == "dst" { r.Host = targetURL.Host } else if t.Host != "" { r.Host = t.Host } // TODO(fs): The HasPrefix check seems redundant since the lookup function should // TODO(fs): have found the target based on the prefix but there may be other // TODO(fs): matchers which may have different rules. I'll keep this for // TODO(fs): a defensive approach. if t.StripPath != "" && strings.HasPrefix(r.URL.Path, t.StripPath) { targetURL.Path = targetURL.Path[len(t.StripPath):] // ensure absolute path after stripping to maintain compliance with // section 5.3 of RFC7230 (https://tools.ietf.org/html/rfc7230#section-5.3) if !strings.HasPrefix(targetURL.Path, "/") { targetURL.Path = "/" + targetURL.Path } } if t.PrependPath != "" { targetURL.Path = t.PrependPath + targetURL.Path // ensure absolute path after stripping to maintain compliance with // section 5.3 of RFC7230 (https://tools.ietf.org/html/rfc7230#section-5.3) if !strings.HasPrefix(targetURL.Path, "/") { targetURL.Path = "/" + targetURL.Path } } if err := addHeaders(r, p.Config, t.StripPath); err != nil { http.Error(w, "cannot parse "+r.RemoteAddr, http.StatusInternalServerError) return } addResponseHeaders(w, r, p.Config) upgrade, accept := r.Header.Get("Upgrade"), r.Header.Get("Accept") tr := p.Transport if t.Transport != nil { tr = t.Transport } else if t.TLSSkipVerify { tr = p.InsecureTransport } var h http.Handler switch { case upgrade == "websocket" || upgrade == "Websocket": r.URL = targetURL if targetURL.Scheme == "https" || targetURL.Scheme == "wss" { h = newWSHandler(targetURL.Host, func(network, address string) (net.Conn, error) { return tls.Dial(network, address, tr.(*http.Transport).TLSClientConfig) }, p.Stats.WSConn) } else { h = newWSHandler(targetURL.Host, net.Dial, p.Stats.WSConn) } case accept == "text/event-stream": // use the flush interval for SSE (server-sent events) // must be > 0s to be effective h = newHTTPProxy(targetURL, tr, p.Config.FlushInterval) default: h = newHTTPProxy(targetURL, tr, p.Config.GlobalFlushInterval) } if p.Config.GZIPContentTypes != nil { h = gzip.NewGzipHandler(h, p.Config.GZIPContentTypes) } timeNow := p.Time if timeNow == nil { timeNow = time.Now } start := timeNow() rw := &responseWriter{w: w} h.ServeHTTP(rw, r) end := timeNow() dur := end.Sub(start) if p.Stats.Requests != nil { p.Stats.Requests.Observe(dur.Seconds()) } if t.Timer != nil { t.Timer.Observe(dur.Seconds()) } if rw.code <= 0 { return } if p.Stats.StatusTimer != nil { p.Stats.StatusTimer.With("code", strconv.Itoa(rw.code)).Observe(dur.Seconds()) } // write access log if p.Logger != nil { p.Logger.Log(&logger.Event{ Start: start, End: end, Request: r, Response: &http.Response{ StatusCode: rw.code, ContentLength: int64(rw.size), }, RequestURL: requestURL, UpstreamAddr: targetURL.Host, UpstreamService: t.Service, UpstreamURL: targetURL, }) } } // responseWriter wraps an http.ResponseWriter to capture the status code and // the size of the response. It also implements http.Hijacker to forward // hijacking the connection to the wrapped writer if supported. type responseWriter struct { w http.ResponseWriter code int size int } func (rw *responseWriter) Header() http.Header { return rw.w.Header() } func (rw *responseWriter) Write(b []byte) (int, error) { n, err := rw.w.Write(b) rw.size += n return n, err } func (rw *responseWriter) WriteHeader(statusCode int) { rw.w.WriteHeader(statusCode) rw.code = statusCode } func (rw *responseWriter) Flush() { if fl, ok := rw.w.(http.Flusher); ok { fl.Flush() } } var errNoHijacker = errors.New("not a hijacker") func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := rw.w.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, errNoHijacker } ================================================ FILE: proxy/inetaf_tcpproxy.go ================================================ package proxy import ( "context" "errors" "fmt" "log" "net" "net/http" "github.com/inetaf/tcpproxy" ) type childProxy struct { l net.Listener s Server } type InetAfTCPProxyServer struct { Proxy *tcpproxy.Proxy children []*childProxy } // Close - implements Server - is this even called? func (tps *InetAfTCPProxyServer) Close() error { _ = tps.Proxy.Close() firstErr := tps.Proxy.Wait() errChan := make(chan error, len(tps.children)) for _, sl := range tps.children { go func(sl *childProxy) { errChan <- sl.s.Close() }(sl) } for range tps.children { err := <-errChan if errors.Is(err, http.ErrServerClosed) { err = nil } if firstErr == nil { firstErr = err } if err != nil { log.Printf("[ERROR] %s", err) } } return firstErr } // Serve - implements server. The listener is ignored, but it // calls serve on the children func (tps *InetAfTCPProxyServer) Serve(_ net.Listener) error { if len(tps.children) == 0 { return fmt.Errorf("no children defined for listener") } errChan := make(chan error, len(tps.children)) for _, sl := range tps.children { go func(sl *childProxy) { errChan <- sl.s.Serve(sl.l) }(sl) } firstErr := tps.Proxy.Wait() for range tps.children { err := <-errChan if errors.Is(err, http.ErrServerClosed) { err = nil } if firstErr == nil { firstErr = err } if err != nil { log.Print("[FATAL] ", err) } } return firstErr } // ServeLater - l is really only for listeners that are // tcpproxy.TargetListener or a derivative. Don't call after // Serve() is called. func (tps *InetAfTCPProxyServer) ServeLater(l net.Listener, s Server) { tps.children = append(tps.children, &childProxy{l, s}) } func (tps *InetAfTCPProxyServer) Shutdown(ctx context.Context) error { _ = tps.Proxy.Close() // always returns nil error anyway firstErr := tps.Proxy.Wait() // wait for outer listener to close before telling the childProxy errChan := make(chan error, len(tps.children)) for _, sl := range tps.children { go func(sl *childProxy) { errChan <- sl.s.Shutdown(ctx) }(sl) } for range tps.children { err := <-errChan if firstErr == nil { firstErr = err } if err != nil { log.Print("[ERROR] ", err) } } return firstErr } ================================================ FILE: proxy/inetaf_tcpproxy_integration_test.go ================================================ package proxy import ( "bytes" "context" "crypto/x509" "fmt" "io" "net" "net/http" "net/http/httptest" "os" "testing" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/proxy/tcp" "github.com/fabiolb/fabio/proxy/tcp/tcptest" "github.com/fabiolb/fabio/route" ) // to run this test, add the following to /etc/hosts: // 127.0.0.1 example.com // 127.0.0.1 example2.com // and then set the environment FABIO_IHAVEHOSTENTRIES=true // This also runs in Github Actions by default, since the workflow adds these aliases. func TestProxyTCPAndHTTPS(t *testing.T) { if os.Getenv("TRAVIS") != "true" && os.Getenv("CI") != "true" && os.Getenv("FABIO_IHAVEHOSTENTRIES") != "true" { t.Skip("skipping because env FABIO_IHAVEHOSTENTRIES is not set to true") } tlsCfg1 := tlsServerConfig() tlsCfg2 := tlsServerConfig2() tcpServer := httptest.NewUnstartedServer(okHandler) tcpServer.TLS = tlsCfg2 tcpServer.StartTLS() defer tcpServer.Close() httpPayload := []byte(`OK HTTP`) httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(httpPayload) })) defer httpServer.Close() tpl := `route add srv / %s opts "proto=https" route add tcproute example2.com/ tcp://%s opts "proto=tcp"` table, _ := route.NewTable(bytes.NewBufferString(fmt.Sprintf(tpl, httpServer.URL, tcpServer.Listener.Addr()))) hp := &HTTPProxy{ Lookup: func(r *http.Request) *route.Target { return table.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, } tp := &tcp.SNIProxy{ Lookup: func(h string) *route.Target { return table.LookupHost(h, route.Picker["rr"]) }, } m := func(_ context.Context, h string) bool { // TODO - matcher needs to move out of main // so we can test it more easily. Probably // the other functions too. t := table.LookupHost(h, route.Picker["rr"]) if t == nil { return false } // Make sure this is supposed to be a tcp proxy. // opts proto= overrides scheme if present. var ( ok bool proto string ) if proto, ok = t.Opts["proto"]; !ok && t.URL != nil { proto = t.URL.Scheme } return proto == "tcp" } // get an unused port for use for the proxy. the rest of the tests just // pick a high-numbered port, but this should be safer, if ugly. could // also just fire up a listener with 0 as the port and let the stack // pick one - which is what httptest does - but this is less lines and // I'm lazy. --NJ tmp := httptest.NewServer(okHandler) proxyAddr := tmp.Listener.Addr().String() tmp.Close() _, port, err := net.SplitHostPort(proxyAddr) if err != nil { t.Fatalf("error determining port from addr: %s", err) } l := config.Listen{Addr: proxyAddr} go func() { err := ListenAndServeHTTPSTCPSNI(l, hp, tp, tlsCfg1, m) if err != nil { t.Logf("error shutting down: %s", err) } }() defer Close() // retry until listener is responding. d, err := tcptest.NewRetryDialer().Dial("tcp", proxyAddr) if err != nil { t.Fatalf("error connecting to proxy: %s", err) } d.Close() // At this point, the proxy should up and listening and will do // tcp proxy to https://example2.com, and terminate TLS for // https://example.com c := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsClientConfig(), DisableKeepAlives: true, MaxConnsPerHost: -1, }, } // make sure tcp steering happens for https://example2.com/ // and https proxying happens for https://example.com/ for _, data := range []struct { name string u string h string body []byte }{{ name: "https proxy for example.com", u: "https://example.com:" + port, h: "example.com", body: httpPayload, }, { name: "tcp proxy for example2.com serving https", u: "https://example2.com:" + port, h: "example2.com", body: []byte(`OK`), }} { t.Run(data.name, func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, data.u, nil) if err != nil { t.Fatalf("unexpected error creating req: %s", err) } resp, err := c.Do(req) if err != nil { t.Errorf("error on request %s", err) return } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { t.Errorf("error reading body: %s", err) return } if !bytes.Equal(body, data.body) { t.Error("http body not equal") } if len(resp.TLS.PeerCertificates) != 1 { t.Errorf("unexpected peer certs") return } if !foundDNSName(resp.TLS.PeerCertificates[0], data.h) { t.Error("wrong certificate returned") } }) } } func foundDNSName(crt *x509.Certificate, dnsName string) bool { found := false for _, dname := range crt.DNSNames { if dname == dnsName { found = true break } } return found } ================================================ FILE: proxy/internal/testcert.go ================================================ package internal // LocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // 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 var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- MIICEzCCAXygAwIBAgIQS3cofn+2H4NxFntgaMRAPTANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDp+sQVBNYwZ4YSskddAtTYq2NPdWYawNw9YQDBU9ft3fIm1r9UoyL/57bo gCgFAkglXo06sAfuk+W6OXRPplEwxCU/mAiAjMLKES1V3oZnI42sTeiskdvb8j6E 47EpbWSA2OU4Nqulbh6vkGrzYzUdlmwwz+rGvfmHp1EOjMVzvQIDAQABo2gwZjAO BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA AAAAATANBgkqhkiG9w0BAQsFAAOBgQBChdgkaHaw83GFx8aDWoE3K4+h9YqXuvEP b2OWAYlzY/U99BA9P0lE4vGpaIAeCFxalJ2AK3yHjt+eezy3sw0bMeG8ZNYcOyIV exS95UdAKFt93a5zIWrkYQvhuzln1IOxPJQZ4rkq4nikLj2WuyGR7QnuVBdgPqP7 RN4BPb5Sog== -----END CERTIFICATE-----`) // LocalhostKey is the private key for LocalhostCert. var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDp+sQVBNYwZ4YSskddAtTYq2NPdWYawNw9YQDBU9ft3fIm1r9U oyL/57bogCgFAkglXo06sAfuk+W6OXRPplEwxCU/mAiAjMLKES1V3oZnI42sTeis kdvb8j6E47EpbWSA2OU4Nqulbh6vkGrzYzUdlmwwz+rGvfmHp1EOjMVzvQIDAQAB AoGAGVoXduOQRaxh5ZK1kslkwJlJaGmjB5EQDAJ/r3LjOZ3LyBOKpaQLfcjgk66X J3vIz2vAR7SdF2elA5mIFb1CnJ4HW4cWHzgFQdUnUtoUNuMPy/9QREFfeag9GMPx dZNiypiKqHDSY5ovUL92gtv5W0/w00lYpFiBaYLl+WHvQ6ECQQDvZpULZCEmZHwL hZun4ObzLwFNZ9sNPgwJybnxVYaolXACeh4Ewur0kZlY9DJMqo7Rz82JWuFarkgU GQK/L231AkEA+jP0+q7jfI8NJqwpWFDjwKiI7fadClcdUgXvW2c5wc2pEe4KiAqs ZOWPGsH7SxigGRLzw01SCoInX5yw689JqQJARIOTPENXyWkQpyuBtLYE4qwdL039 vvh28YYuFQdpFm5ONCdG2A4AuCXDQVYB3zcg0KMsK5c6z3z5W+cchiLI0QJBAIDS ZYz4pNoKEVxbAgKdy1XzsGTNN/gN+GO1+JJYKK23RRidNkDrNe3RIAhH3inBKRUf 4/AnjFkqwDkDRTh0htkCQQDfrRZr+gazwzDTSp23+l6MEbqBbc+TTC3c40zpNj4a egxjd5+SkMj6zXEJxAOgo+LmQDGWsu1YQ+XXL87VPwIP -----END RSA PRIVATE KEY-----`) // LocalhostCert2 is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // 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 var LocalhostCert2 = []byte(`-----BEGIN CERTIFICATE----- MIICNTCCAZ6gAwIBAgIQeD/ltjjdLHO9L2c5eMG5rDANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDzwrR53c4RyXpitbeRD9CY6PRhqYgnrCOBy0GUuGs5hJgMqSMXuIH4Vs4h lOH19hb9o733O+qJM6s4D8GNfz2LC/SC/DOHqXv0DeB6lGJ47I2Wv8569uFjNh3K pi5yYlAqNdjQ1TYjUZmDytiQxp8eCLCKGpbWvjWzop50GTefqwIDAQABo4GJMIGG MA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8E BTADAQH/MB0GA1UdDgQWBBSLtvT+Rtv9N+Tw1ZbXU+jSxYqYxTAvBgNVHREEKDAm ggxleGFtcGxlMi5jb22HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcN AQELBQADgYEAkO5LWq8SHx8dUgMDryCyrJamsFT3Z/Lt1zMfJfNdTRSvsg7Fy3XR IOtxPqh2gT7OsSeU6fjjbDUTuGmH/BckwZTFMkRho/WaEgbP6XWWjkl+6euJBvtG lBFElB/HVPa5puggihR9H1pE3s+SdtslwfOf8XsUA4xlcrhpU5kuMa4= -----END CERTIFICATE-----`) // LocalhostKey2 is the private key for LocalhostCert2. var LocalhostKey2 = []byte(`-----BEGIN PRIVATE KEY----- MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAPPCtHndzhHJemK1 t5EP0Jjo9GGpiCesI4HLQZS4azmEmAypIxe4gfhWziGU4fX2Fv2jvfc76okzqzgP wY1/PYsL9IL8M4epe/QN4HqUYnjsjZa/znr24WM2HcqmLnJiUCo12NDVNiNRmYPK 2JDGnx4IsIoalta+NbOinnQZN5+rAgMBAAECgYEAlRnYuN5SiRC7WpuacBHDX3TG 3uILFXE2utKwB58Sfzk6pCvk+kJyxYubRHFEEeX4RCcfMJYmrMu9BGqm0r0sz6nb CSZk2Crn7eKgLK01+t2K+2s1R2oNB/fxkmxVUTbxiZ+Bt7xsAvFnnQRl06r9NNYo XqAadfRFGlmMkSEAbbkCQQD9feneVqIdPPGQOadUq8VYY/M8onMSG72O0qr6Dx8X j3hXf5D91pvXos+h3TwSICQ354BcakI4VK/EXxfmNUSXAkEA9iwkdXYQXkjgqpwH 3jMxG3DLAl9aHnykFSm8G2vQj2427ePnLppHclXKTPfu3E0qUNT2WgOK64N3qC1F NkZ8DQJBAPSOlKFfnVlt4XOOW8QRT/wduZ4G79NJlhCDaFaFXi7ByI1J0h1C/ekE 9yInKXwnLCoPG0SNc0ObWFOwloMPYxMCQEIH5yOmro9Lxw+cWLPuUU7F+35Aa2Dg F/chQbatPb0rWAqJZhpnAaEWh/QLUQPAowgZh5bvelTf57mxou4DDAUCQHynSBDh GPDiBeTKX+VF50yHR8P6YrbeLIwasw19HA2BVhKKP05ZbQnWtN/ekhhBbI5fTwn0 njeVQ3HbrfnY1Ik= -----END PRIVATE KEY-----`) ================================================ FILE: proxy/listen.go ================================================ package proxy import ( "crypto/tls" "fmt" "github.com/fabiolb/fabio/config" "net" "time" proxyproto "github.com/armon/go-proxyproto" ) func ListenTCP(l config.Listen, cfg *tls.Config) (net.Listener, error) { addr, err := net.ResolveTCPAddr("tcp", l.Addr) if err != nil { return nil, fmt.Errorf("listen: Fail to resolve tcp addr. %s", l.Addr) } var ln net.Listener ln, err = net.ListenTCP("tcp", addr) if err != nil { return nil, fmt.Errorf("listen: Fail to listen. %s", err) } // enable TCPKeepAlive support ln = tcpKeepAliveListener{ln.(*net.TCPListener)} // enable PROXY protocol support if l.ProxyProto { ln = &proxyproto.Listener{ Listener: ln, ProxyHeaderTimeout: l.ProxyHeaderTimeout, } } // enable TLS if cfg != nil { ln = tls.NewListener(ln, cfg) } return &tcpListener{ln, addr, cfg}, nil } type tcpListener struct { l net.Listener addr net.Addr tlsConfig *tls.Config } func (ln *tcpListener) Addr() net.Addr { return ln.addr } func (ln *tcpListener) Accept() (net.Conn, error) { return ln.l.Accept() } func (ln *tcpListener) Close() error { return ln.l.Close() } // copied from http://golang.org/src/net/http/server.go?s=54604:54695#L1967 // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually // go away. type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } if err = tc.SetKeepAlive(true); err != nil { return } if err = tc.SetKeepAlivePeriod(3 * time.Minute); err != nil { return } return tc, nil } ================================================ FILE: proxy/listen_test.go ================================================ package proxy import ( "bytes" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/route" ) func TestGracefulShutdown(t *testing.T) { // start a server which responds after the shutdown has been triggered. trigger := make(chan bool) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-trigger })) defer srv.Close() // start proxy addr := "127.0.0.1:57777" var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() h := &HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add svc / " + srv.URL)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, } l := config.Listen{Addr: addr} if err := ListenAndServeHTTP(l, h, nil); err != nil { t.Log("ListenAndServeHTTP: ", err) } }() // trigger shutdown after some time delay := 100 * time.Millisecond go func() { time.Sleep(delay) close(trigger) Shutdown(delay) }() // give server some time to start up time.Sleep(delay / 2) makeReq := func() (int, error) { resp, err := http.Get("http://" + addr + "/") if err != nil { return 0, err } defer resp.Body.Close() return resp.StatusCode, nil } // make 200 OK request // start before and complete after shutdown was triggered code, err := makeReq() if err != nil { t.Fatalf("request 1: got error %q want nil", err) } if got, want := code, 200; got != want { t.Fatalf("request 1: got %v want %v", got, want) } // make request to closed server _, err = makeReq() if got, want := err.Error(), "connection refused"; !strings.Contains(got, want) { t.Fatalf("request 2: got error %q want %q", got, want) } // wait for listen() to return // note that the actual listeners have not returned yet wg.Wait() } ================================================ FILE: proxy/serve.go ================================================ package proxy import ( "context" "crypto/tls" "errors" "log" "net" "net/http" "sync" "time" "google.golang.org/grpc" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/proxy/tcp" "github.com/armon/go-proxyproto" "github.com/inetaf/tcpproxy" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Server interface { Close() error Serve(l net.Listener) error Shutdown(ctx context.Context) error } var ( // mu guards servers which contains the list // of running proxy servers. mu sync.Mutex servers = make(map[string]Server) ) func CloseProxy(address string) error { mu.Lock() if srv, ok := servers[address]; ok { err := srv.Close() if err != nil { return err } log.Printf("[INFO] Dynamic TCP listener on %s has been terminated", address) delete(servers, address) } mu.Unlock() return nil } func Close() { mu.Lock() for _, srv := range servers { srv.Close() } servers = make(map[string]Server) mu.Unlock() } func Shutdown(timeout time.Duration) { mu.Lock() srvs := make(map[string]Server, len(servers)) for k, v := range servers { srvs[k] = v } servers = make(map[string]Server) mu.Unlock() var wg sync.WaitGroup for _, srv := range srvs { wg.Add(1) go func(srv Server) { defer wg.Done() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() srv.Shutdown(ctx) }(srv) } wg.Wait() } func ListenAndServeHTTP(l config.Listen, h http.Handler, cfg *tls.Config) error { ln, err := ListenTCP(l, cfg) if err != nil { return err } srv := &http.Server{ Addr: l.Addr, Handler: h, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, IdleTimeout: l.IdleTimeout, TLSConfig: cfg, } return serve(ln, srv) } func ListenAndServePrometheus(l config.Listen, pcfg config.Prometheus, cfg *tls.Config) error { ln, err := ListenTCP(l, cfg) if err != nil { return err } mux := http.NewServeMux() if pcfg.Path != "/" { mux.HandleFunc("/", func(rw http.ResponseWriter, _ *http.Request) { rw.Header().Set("Location", pcfg.Path) rw.WriteHeader(http.StatusPermanentRedirect) }) } mux.Handle(pcfg.Path, promhttp.Handler()) srv := &http.Server{ Addr: l.Addr, Handler: mux, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, IdleTimeout: l.IdleTimeout, TLSConfig: cfg, } return serve(ln, srv) } func ListenAndServeHTTPSTCPSNI(l config.Listen, h http.Handler, p tcp.Handler, cfg *tls.Config, m tcpproxy.Matcher) error { // we only want proxy proto enabled on tcp proxies pxyProto := l.ProxyProto l.ProxyProto = false tp := &tcpproxy.Proxy{ ListenFunc: func(net, laddr string) (net.Listener, error) { // cfg is nil here so it's not terminating TLS (yet) return ListenTCP(l, nil) }, } // This inspects SNI for matches. If this succeeds then we Proxy tcp. tcpSNIListener := &tcpproxy.TargetListener{Address: l.Addr} tp.AddSNIMatchRoute(l.Addr, m, tcpSNIListener) // Fallthrough to https httpsListener := &tcpproxy.TargetListener{Address: l.Addr} tp.AddRoute(l.Addr, httpsListener) // Start the listener err := tp.Start() if err != nil { return err } tps := &InetAfTCPProxyServer{Proxy: tp} var tln net.Listener = tcpSNIListener // enable proxy protocol on the tcp side if configured to do so if pxyProto { tln = &proxyproto.Listener{ Listener: tln, ProxyHeaderTimeout: l.ProxyHeaderTimeout, } } tps.ServeLater(tln, &tcp.Server{ Addr: l.Addr, Handler: p, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, }) // wrap TargetListener in a tls terminating version for HTTPS tps.ServeLater(tls.NewListener(httpsListener, cfg), &http.Server{ Addr: l.Addr, Handler: h, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, IdleTimeout: l.IdleTimeout, TLSConfig: cfg, }) // tcpproxy creates its own listener from the configuration above so we can // safely pass nil here, nonetheless we are passing `httpsListener` to // extract it's address and save server in the `servers` map. return serve(httpsListener, tps) } func ListenAndServeGRPC(l config.Listen, opts []grpc.ServerOption, cfg *tls.Config) error { ln, err := ListenTCP(l, cfg) if err != nil { return err } srv := &gRPCServer{ server: grpc.NewServer(opts...), } return serve(ln, srv) } func ListenAndServeTCP(l config.Listen, h tcp.Handler, cfg *tls.Config) error { ln, err := ListenTCP(l, cfg) if err != nil { return err } srv := &tcp.Server{ Addr: l.Addr, Handler: h, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, } return serve(ln, srv) } func serve(ln net.Listener, srv Server) error { mu.Lock() servers[ln.Addr().String()] = srv mu.Unlock() err := srv.Serve(ln) if err != nil { var opErr *net.OpError if errors.Is(err, http.ErrServerClosed) { err = nil } else if errors.As(err, &opErr) { if opErr.Err != nil && opErr.Err.Error() == "use of closed network connection" { err = nil } } } return err } ================================================ FILE: proxy/tcp/copy_buffer.go ================================================ package tcp import ( "io" gkm "github.com/go-kit/kit/metrics" ) // copyBuffer is an adapted version of io.copyBuffer which updates a // counter instead of returning the total bytes written. func copyBuffer(dst io.Writer, src io.Reader, c gkm.Counter) (err error) { buf := make([]byte, 32*1024) for { nr, er := src.Read(buf) if nr > 0 { nw, ew := dst.Write(buf[0:nr]) if nw > 0 { if c != nil { c.Add(float64(nw)) } } if ew != nil { err = ew break } if nr != nw { err = io.ErrShortWrite break } } if er != nil { if er != io.EOF { err = er } break } } return err } ================================================ FILE: proxy/tcp/proxy_proto.go ================================================ package tcp import ( "net" ) // WriteProxyHeader extracts remote and local IP address and port // combinations from incoming connection and writes the PROXY proto // header to the outgoing connection func WriteProxyHeader(out, in net.Conn) error { clientAddr, clientPort, _ := net.SplitHostPort(in.RemoteAddr().String()) serverAddr, serverPort, _ := net.SplitHostPort(in.LocalAddr().String()) var proto string if net.ParseIP(clientAddr).To4() != nil { proto = "TCP4" } else { proto = "TCP6" } header := "PROXY " + proto + " " + clientAddr + " " + serverAddr + " " + clientPort + " " + serverPort + "\r\n" _, err := out.Write([]byte(header)) return err } ================================================ FILE: proxy/tcp/server.go ================================================ package tcp import ( "context" "crypto/tls" "net" "sync" "time" ) // Handler responds to a TCP request. // // ServeTCP should write responses to the in connection and close // it on return. type Handler interface { ServeTCP(in net.Conn) error } type HandlerFunc func(in net.Conn) error func (f HandlerFunc) ServeTCP(in net.Conn) error { return f(in) } // Server implements a generic TCP server. type Server struct { Handler Handler conns map[net.Conn]bool Addr string listeners []net.Listener ReadTimeout time.Duration WriteTimeout time.Duration mu sync.Mutex } func (s *Server) ListenAndServe() error { l, err := net.Listen("tcp", s.Addr) if err != nil { return err } defer l.Close() return s.Serve(l) } func (s *Server) ListenAndServeTLS(certFile, keyFile string) error { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return err } cfg := &tls.Config{Certificates: []tls.Certificate{cert}} l, err := tls.Listen("tcp", s.Addr, cfg) if err != nil { return err } defer l.Close() return s.Serve(l) } func (s *Server) Serve(l net.Listener) error { defer l.Close() s.mu.Lock() s.listeners = append(s.listeners, l) s.mu.Unlock() for { c, err := l.Accept() if err != nil { return err } c = &conn{ c: c, ReadTimeout: s.ReadTimeout, WriteTimeout: s.WriteTimeout, } s.mu.Lock() if s.conns == nil { s.conns = map[net.Conn]bool{} } s.conns[c] = true s.mu.Unlock() go func() { defer func() { c.Close() s.mu.Lock() delete(s.conns, c) s.mu.Unlock() }() s.Handler.ServeTCP(c) }() } } func (s *Server) closeListeners() { s.mu.Lock() for _, l := range s.listeners { l.Close() } s.listeners = nil s.mu.Unlock() } func (s *Server) closeConns() error { s.mu.Lock() for c := range s.conns { c.Close() } s.conns = nil s.mu.Unlock() return nil } func (s *Server) Close() error { s.closeListeners() return s.closeConns() } func (s *Server) Shutdown(ctx context.Context) error { s.closeListeners() if ctx != nil { <-ctx.Done() } return s.closeConns() } // conn implements a connection which honors read and write timeouts. type conn struct { c net.Conn ReadTimeout time.Duration WriteTimeout time.Duration } func (c *conn) Read(b []byte) (int, error) { if c.ReadTimeout > 0 { c.c.SetReadDeadline(time.Now().Add(c.ReadTimeout)) } return c.c.Read(b) } func (c *conn) Write(b []byte) (int, error) { if c.WriteTimeout > 0 { c.c.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) } return c.c.Write(b) } func (c *conn) Close() error { return c.c.Close() } func (c *conn) LocalAddr() net.Addr { return c.c.LocalAddr() } func (c *conn) RemoteAddr() net.Addr { return c.c.RemoteAddr() } func (c *conn) SetDeadline(t time.Time) error { return c.c.SetDeadline(t) } func (c *conn) SetReadDeadline(t time.Time) error { return c.c.SetReadDeadline(t) } func (c *conn) SetWriteDeadline(t time.Time) error { return c.c.SetWriteDeadline(t) } ================================================ FILE: proxy/tcp/sni_proxy.go ================================================ package tcp import ( "bufio" gkm "github.com/go-kit/kit/metrics" "io" "log" "net" "time" "github.com/fabiolb/fabio/route" ) // SNIProxy implements an SNI aware transparent TCP proxy which captures the // TLS client hello, extracts the host name and uses it for finding the // upstream server. Then it replays the ClientHello message and copies data // transparently allowing to route a TLS connection based on the SNI header // without decrypting it. type SNIProxy struct { // Conn counts the number of connections. Conn gkm.Counter // ConnFail counts the failed upstream connection attempts. ConnFail gkm.Counter // Noroute counts the failed Lookup() calls. Noroute gkm.Counter // Lookup returns a target host for the given server name. // The proxy will panic if this value is nil. Lookup func(host string) *route.Target // DialTimeout sets the timeout for establishing the outbound // connection. DialTimeout time.Duration } func (p *SNIProxy) ServeTCP(in net.Conn) error { defer in.Close() if p.Conn != nil { p.Conn.Add(1) } tlsReader := bufio.NewReader(in) tlsHeaders, err := tlsReader.Peek(9) if err != nil { log.Print("[DEBUG] tcp+sni: TLS handshake failed (failed to peek data)") if p.ConnFail != nil { p.ConnFail.Add(1) } return err } bufferSize, err := clientHelloBufferSize(tlsHeaders) if err != nil { log.Printf("[DEBUG] tcp+sni: TLS handshake failed (%s)", err) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } data := make([]byte, bufferSize) _, err = io.ReadFull(tlsReader, data) if err != nil { log.Printf("[DEBUG] tcp+sni: TLS handshake failed (%s)", err) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } // readServerName wants only the handshake message so ignore the first // 5 bytes which is the TLS record header host, ok := readServerName(data[5:]) if !ok { log.Print("[DEBUG] tcp+sni: TLS handshake failed (unable to parse client hello)") if p.ConnFail != nil { p.ConnFail.Add(1) } return nil } if host == "" { log.Print("[DEBUG] tcp+sni: server_name missing") if p.ConnFail != nil { p.ConnFail.Add(1) } return nil } t := p.Lookup(host) if t == nil { if p.Noroute != nil { p.Noroute.Add(1) } return nil } addr := t.URL.Host if t.AccessDeniedTCP(in) { return nil } out, err := net.DialTimeout("tcp", addr, p.DialTimeout) if err != nil { log.Print("[WARN] tcp+sni: cannot connect to upstream ", addr) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } defer out.Close() // enable PROXY protocol support on outbound connection if t.ProxyProto { err := WriteProxyHeader(out, in) if err != nil { log.Print("[WARN] tcp+sni: write proxy protocol header failed. ", err) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } } // write the data already read from the connection n, err := out.Write(data) if err != nil { log.Print("[WARN] tcp+sni: copy client hello failed. ", err) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader, c gkm.Counter) { errc <- copyBuffer(dst, src, c) } // we've received the ClientHello already if t.RxCounter != nil { t.RxCounter.Add(float64(n)) } go cp(in, out, t.RxCounter) go cp(out, in, t.TxCounter) err = <-errc if err != nil && err != io.EOF { log.Print("[WARN]: tcp+sni: ", err) return err } return nil } ================================================ FILE: proxy/tcp/tcp_dynamic_proxy.go ================================================ package tcp import ( gkm "github.com/go-kit/kit/metrics" "io" "log" "net" "time" "github.com/fabiolb/fabio/route" ) // Proxy implements a generic TCP proxying handler. type DynamicProxy struct { // Conn counts the number of connections. Conn gkm.Counter // ConnFail counts the failed upstream connection attempts. ConnFail gkm.Counter // Noroute counts the failed Lookup() calls. Noroute gkm.Counter // Lookup returns a target host for the given request. // The proxy will panic if this value is nil. Lookup func(host string) *route.Target // DialTimeout sets the timeout for establishing the outbound // connection. DialTimeout time.Duration } func (p *DynamicProxy) ServeTCP(in net.Conn) error { defer in.Close() if p.Conn != nil { p.Conn.Add(1) } target := in.LocalAddr().String() t := p.Lookup(target) if t == nil { _, port, _ := net.SplitHostPort(target) t = p.Lookup(":" + port) } if t == nil { if p.Noroute != nil { p.Noroute.Add(1) } return nil } addr := t.URL.Host log.Printf("[DEBUG] Connection: %s incoming %s to %s: ", in.RemoteAddr(), target, addr) if t.AccessDeniedTCP(in) { return nil } out, err := net.DialTimeout("tcp", addr, p.DialTimeout) if err != nil { log.Print("[WARN] tcp: cannot connect to upstream ", addr) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } defer out.Close() errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader, c gkm.Counter) { errc <- copyBuffer(dst, src, c) } go cp(in, out, t.RxCounter) go cp(out, in, t.TxCounter) err = <-errc if err != nil && err != io.EOF { log.Print("[WARN]: tcp: ", err) return err } return nil } ================================================ FILE: proxy/tcp/tcp_proxy.go ================================================ package tcp import ( gkm "github.com/go-kit/kit/metrics" "io" "log" "net" "time" "github.com/fabiolb/fabio/route" ) // Proxy implements a generic TCP proxying handler. type Proxy struct { // Conn counts the number of connections. Conn gkm.Counter // ConnFail counts the failed upstream connection attempts. ConnFail gkm.Counter // Noroute counts the failed Lookup() calls. Noroute gkm.Counter // Lookup returns a target host for the given request. // The proxy will panic if this value is nil. Lookup func(host string) *route.Target // DialTimeout sets the timeout for establishing the outbound // connection. DialTimeout time.Duration } func (p *Proxy) ServeTCP(in net.Conn) error { defer in.Close() if p.Conn != nil { p.Conn.Add(1) } _, port, _ := net.SplitHostPort(in.LocalAddr().String()) port = ":" + port t := p.Lookup(port) if t == nil { if p.Noroute != nil { p.Noroute.Add(1) } return nil } addr := t.URL.Host if t.AccessDeniedTCP(in) { return nil } out, err := net.DialTimeout("tcp", addr, p.DialTimeout) if err != nil { log.Print("[WARN] tcp: cannot connect to upstream ", addr) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } defer out.Close() // enable PROXY protocol support on outbound connection if t.ProxyProto { err := WriteProxyHeader(out, in) if err != nil { log.Print("[WARN] tcp: write proxy protocol header failed. ", err) if p.ConnFail != nil { p.ConnFail.Add(1) } return err } } errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader, c gkm.Counter) { errc <- copyBuffer(dst, src, c) } go cp(in, out, t.RxCounter) go cp(out, in, t.TxCounter) err = <-errc if err != nil && err != io.EOF { log.Print("[WARN]: tcp: ", err) return err } return nil } ================================================ FILE: proxy/tcp/tcptest/dialer.go ================================================ package tcptest import ( "crypto/tls" "net" "time" ) type Dialer interface { Dial(network, addr string) (net.Conn, error) } func NewRetryDialer() *RetryDialer { return &RetryDialer{} } // RetryDialer retries the Dial function until it succeeds or // the timeout has been reached. The default timeout is one // second and the default sleep interval is 100ms. type RetryDialer struct { Dialer net.Dialer Timeout time.Duration Sleep time.Duration ProxyProto bool } func (d *RetryDialer) Dial(network, addr string) (c net.Conn, err error) { dial := func() (net.Conn, error) { conn, err := d.Dialer.Dial(network, addr) if err != nil { return nil, err } if d.ProxyProto { pxy := "PROXY TCP4 1.2.3.4 5.6.7.8 12345 54321\r\n" conn.Write([]byte(pxy)) } return conn, err } return retry(dial, d.Timeout, d.Sleep) } func NewTLSRetryDialer(cfg *tls.Config) *TLSRetryDialer { return &TLSRetryDialer{TLS: cfg} } type TLSRetryDialer struct { TLS *tls.Config Dialer net.Dialer Timeout time.Duration Sleep time.Duration ProxyProto bool } func (d *TLSRetryDialer) Dial(network, addr string) (c net.Conn, err error) { dial := func() (net.Conn, error) { conn, err := net.Dial(network, addr) if err != nil { return nil, err } if d.ProxyProto { pxy := "PROXY TCP4 1.2.3.4 5.6.7.8 12345 54321\r\n" conn.Write([]byte(pxy)) } return tls.Client(conn, d.TLS), nil } return retry(dial, d.Timeout, d.Sleep) } type dialer func() (net.Conn, error) func retry(dial dialer, timeout, sleep time.Duration) (c net.Conn, err error) { if sleep == 0 { sleep = 100 * time.Millisecond } if timeout == 0 { timeout = time.Second } deadline := time.Now().Add(timeout) for { c, err = dial() if err != nil && time.Now().Before(deadline) { time.Sleep(sleep) continue } return } } ================================================ FILE: proxy/tcp/tcptest/server.go ================================================ package tcptest import ( "crypto/tls" "fmt" "net" "time" proxyproto "github.com/armon/go-proxyproto" "github.com/fabiolb/fabio/proxy/internal" "github.com/fabiolb/fabio/proxy/tcp" ) // Server is a TCP test server that binds to a random port. type Server struct { Listener net.Listener // TLS is the optional TLS configuration, populated with a new config // after TLS is started. If set on an unstarted server before StartTLS // is called, existing fields are copied into the new config. TLS *tls.Config // Config may be changed after calling NewUnstartedServer and // before Start or StartTLS. Config *tcp.Server // srv is the actual running server. srv *tcp.Server // Addr is the address the server is listening on in the form ipaddr:port. Addr string } func (s *Server) Start() { if s.Addr != "" { panic("Server already started") } s.Addr = s.Listener.Addr().String() s.srv = new(tcp.Server) s.srv.Addr = s.Config.Addr s.srv.Handler = s.Config.Handler s.srv.ReadTimeout = s.Config.ReadTimeout s.srv.WriteTimeout = s.Config.WriteTimeout go s.srv.Serve(s.Listener) } func (s *Server) StartTLS() { if s.Addr != "" { panic("Server already started") } s.Addr = s.Listener.Addr().String() s.srv = new(tcp.Server) s.srv.Addr = s.Config.Addr s.srv.Handler = s.Config.Handler s.srv.ReadTimeout = s.Config.ReadTimeout s.srv.WriteTimeout = s.Config.WriteTimeout cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey) if err != nil { panic(fmt.Sprintf("tcptest: NewTLSServer: %v", err)) } existingConfig := s.TLS if existingConfig != nil { s.TLS = existingConfig.Clone() } else { s.TLS = new(tls.Config) } if len(s.TLS.Certificates) == 0 { s.TLS.Certificates = []tls.Certificate{cert} } s.Listener = tls.NewListener(s.Listener, s.TLS) go s.srv.Serve(s.Listener) } func (s *Server) Close() error { if s.Addr == "" { panic("Server not started") } return s.srv.Close() } func NewServer(h tcp.Handler) *Server { srv := NewUnstartedServer(h) srv.Start() return srv } func NewTLSServer(h tcp.Handler) *Server { srv := NewUnstartedServer(h) srv.StartTLS() return srv } func NewUnstartedServer(h tcp.Handler) *Server { return &Server{ Listener: newLocalListener(), Config: &tcp.Server{Handler: h}, } } func newLocalListener() net.Listener { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { l, err = net.Listen("tcp6", "[::1]:0") if err != nil { panic("tcptest: Failed to listen on a port: " + err.Error()) } } return l } func NewServerWithProxyProto(h tcp.Handler) *Server { srv := NewUnstartedServerWithProxyProto(h) srv.Start() return srv } func NewTLSServerWithProxyProto(h tcp.Handler) *Server { srv := NewUnstartedServerWithProxyProto(h) srv.StartTLS() return srv } func NewUnstartedServerWithProxyProto(h tcp.Handler) *Server { return &Server{ Listener: &proxyproto.Listener{ Listener: newLocalListener(), ProxyHeaderTimeout: 100 * time.Millisecond, }, Config: &tcp.Server{Handler: h}, } } ================================================ FILE: proxy/tcp/tls_clienthello.go ================================================ package tcp import "errors" // Determines the required size of a buffer large enough to hold // a client hello message including the tls record header and the // handshake message header. // The function requires at least the first 9 bytes of the tls conversation // in "data". // An error is returned if the data does not follow the // specification (https://tools.ietf.org/html/rfc5246) or if the client hello // is fragmented over multiple records. func clientHelloBufferSize(data []byte) (int, error) { // TLS record header // ----------------- // byte 0: rec type (should be 0x16 == Handshake) // byte 1-2: version (should be 0x3000 < v < 0x3003) // byte 3-4: rec len if len(data) < 9 { return 0, errors.New("at least 9 bytes required to determine client hello length") } if data[0] != 0x16 { return 0, errors.New("not a TLS handshake") } recordLength := int(data[3])<<8 | int(data[4]) if recordLength <= 0 || recordLength > 16384 { return 0, errors.New("invalid TLS record length") } // Handshake record header // ----------------------- // byte 5: hs msg type (should be 0x01 == client_hello) // byte 6-8: hs msg len if data[5] != 0x01 { return 0, errors.New("not a client hello") } handshakeLength := int(data[6])<<16 | int(data[7])<<8 | int(data[8]) if handshakeLength <= 0 || handshakeLength > recordLength-4 { return 0, errors.New("invalid client hello length (fragmentation not implemented)") } return handshakeLength + 9, nil //9 for the header bytes } // readServerName returns the server name from a TLS ClientHello message which // has the server_name extension (SNI). ok is set to true if the ClientHello // message was parsed successfully. If the server_name extension was not set // an empty string is returned as serverName. // clientHelloHandshakeMsg must contain the full client hello handshake // message including the 4 byte header. // See: https://www.ietf.org/rfc/rfc5246.txt func readServerName(clientHelloHandshakeMsg []byte) (serverName string, ok bool) { m := new(clientHelloMsg) if !m.unmarshal(clientHelloHandshakeMsg) { //println("client_hello unmarshal failed") return "", false } return m.serverName, true } // The code below is a verbatim copy from go1.7/src/crypto/tls/handshake_messages.go // with some parts commented out. It does enough work to parse a TLS client hello // message and extract the server name extension since this is all we care about. // // Copyright (c) 2016 The Go Authors // TLS extension numbers const ( extensionServerName uint16 = 0 // extensionStatusRequest uint16 = 5 // extensionSupportedCurves uint16 = 10 // extensionSupportedPoints uint16 = 11 // extensionSignatureAlgorithms uint16 = 13 // extensionALPN uint16 = 16 // extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6 // extensionSessionTicket uint16 = 35 // extensionNextProtoNeg uint16 = 13172 // not IANA assigned // extensionRenegotiationInfo uint16 = 0xff01 ) type clientHelloMsg struct { serverName string raw []byte random []byte sessionId []byte compressionMethods []uint8 sessionTicket []uint8 alpnProtocols []string vers uint16 nextProtoNeg bool ocspStapling bool scts bool ticketSupported bool } func (m *clientHelloMsg) unmarshal(data []byte) bool { if len(data) < 42 { return false } m.raw = data m.vers = uint16(data[4])<<8 | uint16(data[5]) m.random = data[6:38] sessionIdLen := int(data[38]) if sessionIdLen > 32 || len(data) < 39+sessionIdLen { return false } m.sessionId = data[39 : 39+sessionIdLen] data = data[39+sessionIdLen:] if len(data) < 2 { return false } // cipherSuiteLen is the number of bytes of cipher suite numbers. Since // they are uint16s, the number must be even. cipherSuiteLen := int(data[0])<<8 | int(data[1]) if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { return false } // numCipherSuites := cipherSuiteLen / 2 // m.cipherSuites = make([]uint16, numCipherSuites) // for i := 0; i < numCipherSuites; i++ { // m.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i]) // if m.cipherSuites[i] == scsvRenegotiation { // m.secureRenegotiationSupported = true // } // } data = data[2+cipherSuiteLen:] if len(data) < 1 { return false } compressionMethodsLen := int(data[0]) if len(data) < 1+compressionMethodsLen { return false } m.compressionMethods = data[1 : 1+compressionMethodsLen] data = data[1+compressionMethodsLen:] m.nextProtoNeg = false m.serverName = "" m.ocspStapling = false m.ticketSupported = false m.sessionTicket = nil // m.signatureAndHashes = nil m.alpnProtocols = nil m.scts = false if len(data) == 0 { // ClientHello is optionally followed by extension data return true } if len(data) < 2 { return false } extensionsLength := int(data[0])<<8 | int(data[1]) data = data[2:] if extensionsLength != len(data) { return false } for len(data) != 0 { if len(data) < 4 { return false } extension := uint16(data[0])<<8 | uint16(data[1]) length := int(data[2])<<8 | int(data[3]) data = data[4:] if len(data) < length { return false } switch extension { case extensionServerName: d := data[:length] if len(d) < 2 { return false } namesLen := int(d[0])<<8 | int(d[1]) d = d[2:] if len(d) != namesLen { return false } for len(d) > 0 { if len(d) < 3 { return false } nameType := d[0] nameLen := int(d[1])<<8 | int(d[2]) d = d[3:] if len(d) < nameLen { return false } if nameType == 0 { m.serverName = string(d[:nameLen]) break } d = d[nameLen:] } // case extensionNextProtoNeg: // if length > 0 { // return false // } // m.nextProtoNeg = true // case extensionStatusRequest: // m.ocspStapling = length > 0 && data[0] == statusTypeOCSP // case extensionSupportedCurves: // // http://tools.ietf.org/html/rfc4492#section-5.5.1 // if length < 2 { // return false // } // l := int(data[0])<<8 | int(data[1]) // if l%2 == 1 || length != l+2 { // return false // } // numCurves := l / 2 // m.supportedCurves = make([]CurveID, numCurves) // d := data[2:] // for i := 0; i < numCurves; i++ { // m.supportedCurves[i] = CurveID(d[0])<<8 | CurveID(d[1]) // d = d[2:] // } // case extensionSupportedPoints: // // http://tools.ietf.org/html/rfc4492#section-5.5.2 // if length < 1 { // return false // } // l := int(data[0]) // if length != l+1 { // return false // } // m.supportedPoints = make([]uint8, l) // copy(m.supportedPoints, data[1:]) // case extensionSessionTicket: // // http://tools.ietf.org/html/rfc5077#section-3.2 // m.ticketSupported = true // m.sessionTicket = data[:length] // case extensionSignatureAlgorithms: // // https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 // if length < 2 || length&1 != 0 { // return false // } // l := int(data[0])<<8 | int(data[1]) // if l != length-2 { // return false // } // n := l / 2 // d := data[2:] // m.signatureAndHashes = make([]signatureAndHash, n) // for i := range m.signatureAndHashes { // m.signatureAndHashes[i].hash = d[0] // m.signatureAndHashes[i].signature = d[1] // d = d[2:] // } // case extensionRenegotiationInfo: // if length == 0 { // return false // } // d := data[:length] // l := int(d[0]) // d = d[1:] // if l != len(d) { // return false // } // m.secureRenegotiation = d // m.secureRenegotiationSupported = true // case extensionALPN: // if length < 2 { // return false // } // l := int(data[0])<<8 | int(data[1]) // if l != length-2 { // return false // } // d := data[2:length] // for len(d) != 0 { // stringLen := int(d[0]) // d = d[1:] // if stringLen == 0 || stringLen > len(d) { // return false // } // m.alpnProtocols = append(m.alpnProtocols, string(d[:stringLen])) // d = d[stringLen:] // } // case extensionSCT: // m.scts = true // if length != 0 { // return false // } } data = data[length:] } return true } ================================================ FILE: proxy/tcp/tls_clienthello_test.go ================================================ package tcp import ( "encoding/hex" "testing" ) func TestClientHelloBufferSize(t *testing.T) { tests := []struct { name string data []byte size int fail bool }{ { name: "valid data", // Largest possible client hello message // |- 16384 -| |----- 16380 ----| data: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x3f, 0xfc}, size: 16384 + 5, // max record length + record header fail: false, }, { name: "not enough data", data: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x3f}, size: 0, fail: true, }, { name: "not a TLS record", data: []byte{0x15, 0x03, 0x01, 0x01, 0xF4, 0x01, 0x00, 0x01, 0xeb}, size: 0, fail: true, }, { name: "TLS record too large", // | max + 1 | data: []byte{0x16, 0x03, 0x01, 0x40, 0x01, 0x01, 0x00, 0x3f, 0xfc}, size: 0, fail: true, }, { name: "TLS record length zero", // |----------| data: []byte{0x16, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x3f, 0xfc}, size: 0, fail: true, }, { name: "Not a client hello", // |----| data: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x02, 0x00, 0x3f, 0xfc}, size: 0, fail: true, }, { name: "Invalid handshake message record length", // |----- 0 --------| data: []byte{0x16, 0x03, 0x01, 0x40, 0x00, 0x01, 0x00, 0x00, 0x00}, size: 0, fail: true, }, { name: "Fragmentation (handshake message larger than record)", // |- 500 ---| |----- 497 ------| data: []byte{0x16, 0x03, 0x01, 0x01, 0xF4, 0x01, 0x00, 0x01, 0xf1}, size: 0, fail: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := clientHelloBufferSize(tt.data) if tt.fail && err == nil { t.Fatal("expected error, got nil") } else if !tt.fail && err != nil { t.Fatalf("expected error to be nil, got %s", err) } if want := tt.size; got != want { t.Fatalf("want size %d, got %d", want, got) } }) } } func TestReadServerName(t *testing.T) { tests := []struct { name string servername string ok bool data string //Hex string, decoded by test }{ { // Client hello from: // openssl s_client -connect google.com:443 -servername google.com name: "valid client hello with server name", servername: "google.com", ok: true, data: "0100014803032657cacce41598fa82e5b75061050bc31c5affdba106b8e7431852" + "24af0fa1aa000098cc14cc13cc15c030c02cc028c024c014c00a00a3009f00" + "6b006a00390038ff8500c400c3008800870081c032c02ec02ac026c00fc005" + "009d003d003500c00084c02fc02bc027c023c013c00900a2009e0067004000" + "33003200be00bd00450044c031c02dc029c025c00ec004009c003c002f00ba" + "0041c011c007c00cc00200050004c012c00800160013c00dc003000a001500" + "12000900ff010000870000000f000d00000a676f6f676c652e636f6d000b00" + "0403000102000a003a0038000e000d0019001c000b000c001b00180009000a" + "001a0016001700080006000700140015000400050012001300010002000300" + "0f0010001100230000000d00260024060106020603efef0501050205030401" + "04020403eeeeeded030103020303020102020203", }, { // Client hello from: // openssl s_client -connect google.com:443 name: "valid client hello but no server name extension", servername: "", ok: true, data: "0100013503036dfb09de7b16503dd1bb304dcbe54079913b65abf53de997f73b26c99e" + "67ba28000098cc14cc13cc15c030c02cc028c024c014c00a00a3009f006b006a00" + "390038ff8500c400c3008800870081c032c02ec02ac026c00fc005009d003d0035" + "00c00084c02fc02bc027c023c013c00900a2009e006700400033003200be00bd00" + "450044c031c02dc029c025c00ec004009c003c002f00ba0041c011c007c00cc002" + "00050004c012c00800160013c00dc003000a00150012000900ff01000074000b00" + "0403000102000a003a0038000e000d0019001c000b000c001b00180009000a001a" + "00160017000800060007001400150004000500120013000100020003000f001000" + "1100230000000d00260024060106020603efef050105020503040104020403eeee" + "eded030103020303020102020203", }, { name: "invalid client hello", servername: "", ok: false, data: "0100014c5768656e2070656f706c652073617920746f206d653a20776f756c6420796f" + "75207261746865722062652074686f75676874206f6620617320612066756e6e79" + "206d616e206f72206120677265617420626f73733f204d7920616e737765722773" + "20616c77617973207468652073616d652c20746f206d652c207468657927726520" + "6e6f74206d757475616c6c79206578636c75736976652e2d204461766964204272" + "656e74", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clientHelloMsg, _ := hex.DecodeString(tt.data) servername, ok := readServerName(clientHelloMsg) if got, want := servername, tt.servername; got != want { t.Fatalf("%s: got servername \"%s\" want \"%s\"", tt.name, got, want) } if got, want := ok, tt.ok; got != want { t.Fatalf("%s: got ok %t want %t", tt.name, got, want) } }) } } ================================================ FILE: proxy/tcp_integration_test.go ================================================ package proxy import ( "bufio" "bytes" "crypto/tls" "crypto/x509" "net" "net/url" "os" "path/filepath" "testing" "time" "github.com/fabiolb/fabio/cert" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/proxy/internal" "github.com/fabiolb/fabio/proxy/tcp" "github.com/fabiolb/fabio/proxy/tcp/tcptest" "github.com/fabiolb/fabio/route" ) var echoHandler tcp.HandlerFunc = func(c net.Conn) error { defer c.Close() line, _, err := bufio.NewReader(c).ReadLine() if err != nil { return err } line = append(line, []byte(" echo")...) _, err = c.Write(line) return err } // TestTCPDynamicProxy tests proxying an unencrypted TCP connection // to a TCP upstream server. func TestTCPDyanmicProxy(t *testing.T) { srv := tcptest.NewServer(echoHandler) defer srv.Close() // start proxy proxyAddr := "127.0.0.1:57778" go func() { h := &tcp.DynamicProxy{ Lookup: func(h string) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv 127.0.0.1:57778 tcp://" + srv.Addr)) return tbl.LookupHost(h, route.Picker["rr"]) }, } l := config.Listen{Addr: proxyAddr} if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() defer Close() // connect to proxy out, err := tcptest.NewRetryDialer().Dial("tcp", proxyAddr) if err != nil { t.Fatalf("net.Dial: %#v", err) } defer out.Close() testRoundtrip(t, out) } // TestTCPProxy tests proxying an unencrypted TCP connection // to a TCP upstream server. func TestTCPProxy(t *testing.T) { srv := tcptest.NewServer(echoHandler) defer srv.Close() // start proxy proxyAddr := "127.0.0.1:57778" go func() { h := &tcp.Proxy{ Lookup: func(h string) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv :57778 tcp://" + srv.Addr)) return tbl.LookupHost(h, route.Picker["rr"]) }, } l := config.Listen{Addr: proxyAddr} if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() defer Close() // connect to proxy out, err := tcptest.NewRetryDialer().Dial("tcp", proxyAddr) if err != nil { t.Fatalf("net.Dial: %#v", err) } defer out.Close() testRoundtrip(t, out) } // TestTCPProxyWithTLS tests proxying an encrypted TCP connection // to an unencrypted upstream TCP server. The proxy terminates the // TLS connection. func TestTCPProxyWithTLS(t *testing.T) { srv := tcptest.NewServer(echoHandler) defer srv.Close() // setup cert source dir := t.TempDir() mustWrite := func(name string, data []byte) { path := filepath.Join(dir, name) if err := os.WriteFile(path, data, 0644); err != nil { t.Fatalf("os.WriteFile: %s", err) } } mustWrite("example.com-key.pem", internal.LocalhostKey) mustWrite("example.com-cert.pem", internal.LocalhostCert) // start tcp proxy proxyAddr := "127.0.0.1:57779" cs := config.CertSource{Name: "cs", Type: "path", CertPath: dir} src, err := cert.NewSource(cs) if err != nil { t.Fatal("cert.NewSource: ", err) } tlscfg, err := cert.TLSConfig(src, false, 0, 0, nil) if err != nil { t.Fatal("cert.TLSConfig: ", err) } go func() { h := &tcp.Proxy{ Lookup: func(string) *route.Target { return &route.Target{URL: &url.URL{Host: srv.Addr}} }, } l := config.Listen{Addr: proxyAddr} if err := ListenAndServeTCP(l, h, tlscfg); err != nil { // closing the listener returns this error from the accept loop // which we can ignore. if err.Error() != "accept tcp 127.0.0.1:57779: use of closed network connection" { t.Log("ListenAndServeTCP: ", err) } } }() defer Close() // give cert store some time to pick up certs time.Sleep(250 * time.Millisecond) rootCAs := x509.NewCertPool() if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { t.Fatal("could not parse cert") } cfg := &tls.Config{ RootCAs: rootCAs, ServerName: "example.com", } // connect to proxy out, err := tcptest.NewTLSRetryDialer(cfg).Dial("tcp", proxyAddr) if err != nil { t.Fatalf("tls.Dial: %#v", err) } defer out.Close() testRoundtrip(t, out) } // TestTCPSNIProxy tests proxying an encrypted TCP connection // to an upstream TCP service without decrypting the traffic. // The upstream server terminates the TLS connection. func TestTCPSNIProxy(t *testing.T) { srv := tcptest.NewTLSServer(echoHandler) defer srv.Close() // start tcp proxy proxyAddr := "127.0.0.1:57778" go func() { h := &tcp.SNIProxy{ Lookup: func(string) *route.Target { return &route.Target{URL: &url.URL{Host: srv.Addr}} }, } l := config.Listen{Addr: proxyAddr} if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() defer Close() rootCAs := x509.NewCertPool() if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { t.Fatal("could not parse cert") } cfg := &tls.Config{ RootCAs: rootCAs, ServerName: "example.com", } // connect to proxy out, err := tcptest.NewTLSRetryDialer(cfg).Dial("tcp", proxyAddr) if err != nil { t.Fatalf("tls.Dial: %#v", err) } defer out.Close() testRoundtrip(t, out) } func testRoundtrip(t *testing.T, c net.Conn) { // send data to server _, err := c.Write([]byte("foo\n")) if err != nil { t.Fatal("out.Write: ", err) } // read response which should be // src data + " echo" line, _, err := bufio.NewReader(c).ReadLine() if err != nil { t.Fatal("readLine: ", err) } // compare if got, want := line, []byte("foo echo"); !bytes.Equal(got, want) { t.Fatalf("got %q want %q", got, want) } } var proxyHandler tcp.HandlerFunc = func(c net.Conn) error { defer c.Close() line, _, err := bufio.NewReader(c).ReadLine() if err != nil { return err } str := " " + c.RemoteAddr().String() line = append(line, []byte(str)...) _, err = c.Write(line) return err } // TestTCPProxyWithProxyProtoEnables tests proxying an unencrypted TCP connection // to a TCP upstream server with proxy protocol enabed on upstream connection func TestTCPProxyWithProxyProto(t *testing.T) { srv := tcptest.NewServerWithProxyProto(proxyHandler) defer srv.Close() // start proxy proxyAddr := "127.0.0.1:57778" go func() { h := &tcp.Proxy{ Lookup: func(h string) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv :57778 tcp://" + srv.Addr + " opts \"pxyproto=true\"")) tgt := tbl.LookupHost(h, route.Picker["rr"]) return tgt }, } l := config.Listen{Addr: proxyAddr, ProxyProto: true} if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() defer Close() // connect to proxy dialer := tcptest.NewRetryDialer() dialer.ProxyProto = true out, err := dialer.Dial("tcp", proxyAddr) if err != nil { t.Fatalf("net.Dial: %#v", err) } defer out.Close() testProxyProto(t, out) } // TestTCPProxyWithTLSWithProxyProto tests proxying an encrypted TCP connection // to an unencrypted upstream TCP server with proxy protocol enabled. // The proxy extract the proxy protocol header and terminates the TLS connection. func TestTCPProxyWithTLSWithProxyProto(t *testing.T) { srv := tcptest.NewServerWithProxyProto(proxyHandler) defer srv.Close() // setup cert source dir := t.TempDir() mustWrite := func(name string, data []byte) { path := filepath.Join(dir, name) if err := os.WriteFile(path, data, 0644); err != nil { t.Fatalf("os.WriteFile: %s", err) } } mustWrite("example.com-key.pem", internal.LocalhostKey) mustWrite("example.com-cert.pem", internal.LocalhostCert) // start tcp proxy proxyAddr := "127.0.0.1:57779" cs := config.CertSource{Name: "cs", Type: "path", CertPath: dir} src, err := cert.NewSource(cs) if err != nil { t.Fatal("cert.NewSource: ", err) } tlscfg, err := cert.TLSConfig(src, false, 0, 0, nil) if err != nil { t.Fatal("cert.TLSConfig: ", err) } go func() { h := &tcp.Proxy{ Lookup: func(string) *route.Target { return &route.Target{URL: &url.URL{Host: srv.Addr}, ProxyProto: true} }, } l := config.Listen{Addr: proxyAddr, ProxyProto: true} if err := ListenAndServeTCP(l, h, tlscfg); err != nil { // closing the listener returns this error from the accept loop // which we can ignore. if err.Error() != "accept tcp 127.0.0.1:57779: use of closed network connection" { t.Log("ListenAndServeTCP: ", err) } } }() defer Close() // give cert store some time to pick up certs time.Sleep(250 * time.Millisecond) rootCAs := x509.NewCertPool() if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { t.Fatal("could not parse cert") } cfg := &tls.Config{ RootCAs: rootCAs, ServerName: "example.com", } // connect to proxy dialer := tcptest.NewTLSRetryDialer(cfg) dialer.ProxyProto = true out, err := dialer.Dial("tcp", proxyAddr) if err != nil { t.Fatalf("tls.Dial: %#v", err) } defer out.Close() testProxyProto(t, out) } // TestTCPSNIProxyWithProxyProto tests proxying an encrypted TCP connection adding // proxy protocol header to an upstream TCP service without decrypting the traffic. // The upstream server extracts the proxy protocol and terminates the TLS connection. func TestTCPSNIProxyWithProxyProto(t *testing.T) { srv := tcptest.NewTLSServerWithProxyProto(proxyHandler) defer srv.Close() // start tcp proxy proxyAddr := "127.0.0.1:57778" go func() { h := &tcp.SNIProxy{ Lookup: func(string) *route.Target { return &route.Target{URL: &url.URL{Host: srv.Addr}, ProxyProto: true} }, } l := config.Listen{Addr: proxyAddr, ProxyProto: true} if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() defer Close() rootCAs := x509.NewCertPool() if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { t.Fatal("could not parse cert") } cfg := &tls.Config{ RootCAs: rootCAs, ServerName: "example.com", } // connect to proxy dialer := tcptest.NewTLSRetryDialer(cfg) dialer.ProxyProto = true out, err := dialer.Dial("tcp", proxyAddr) if err != nil { t.Fatalf("tls.Dial: %#v", err) } defer out.Close() testProxyProto(t, out) } func testProxyProto(t *testing.T, c net.Conn) { // send data to server _, err := c.Write([]byte("foo\n")) if err != nil { t.Fatal("out.Write: ", err) } // read response which should be // PROXY proto header line, _, err := bufio.NewReader(c).ReadLine() if err != nil { t.Fatal("readLine: ", err) } // remote := c.RemoteAddr().String() // local := c.LocalAddr().String() // compare if got, want := line, []byte("foo 1.2.3.4:12345"); !bytes.Equal(got, want) { t.Fatalf("got %q want %q", got, want) } } ================================================ FILE: proxy/ws_handler.go ================================================ package proxy import ( "bytes" gkm "github.com/go-kit/kit/metrics" "io" "log" "net" "net/http" "strings" "sync/atomic" "time" ) // conns keeps track of the number of open ws connections var conns int64 type dialFunc func(network, address string) (net.Conn, error) // newWSHandler returns an HTTP handler which forwards data between // an incoming and outgoing websocket connection. It checks whether // the handshake was completed successfully before forwarding data // between the client and server. func newWSHandler(host string, dial dialFunc, conn gkm.Gauge) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if conn != nil { conn.Set(float64(atomic.AddInt64(&conns, 1))) defer func() { conn.Set(float64(atomic.AddInt64(&conns, -1))) }() } hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "not a hijacker", http.StatusInternalServerError) return } in, _, err := hj.Hijack() if err != nil { log.Printf("[ERROR] Hijack error for %s. %s", r.URL, err) http.Error(w, "hijack error", http.StatusInternalServerError) return } defer in.Close() out, err := dial("tcp", host) if err != nil { log.Printf("[ERROR] WS error for %s. %s", r.URL, err) http.Error(w, "error contacting backend server", http.StatusInternalServerError) return } defer out.Close() err = r.Write(out) if err != nil { log.Printf("[ERROR] Error copying request for %s. %s", r.URL, err) http.Error(w, "error copying request", http.StatusInternalServerError) return } // read the initial response to check whether we get an HTTP/1.1 101 ... response // to determine whether the handshake worked. b := make([]byte, 1024) if err := out.SetReadDeadline(time.Now().Add(time.Second)); err != nil { log.Printf("[ERROR] Error setting read timeout for %s: %s", r.URL, err) http.Error(w, "error setting read timeout", http.StatusInternalServerError) return } n, err := out.Read(b) if err != nil { log.Printf("[ERROR] Error reading handshake for %s: %s", r.URL, err) http.Error(w, "error reading handshake", http.StatusInternalServerError) return } b = b[:n] if m, err := in.Write(b); err != nil || n != m { log.Printf("[ERROR] Error sending handshake for %s: %s", r.URL, err) http.Error(w, "error sending handshake", http.StatusInternalServerError) return } // https://tools.ietf.org/html/rfc6455#section-1.3 // The websocket server must respond with HTTP/1.1 101 on successful handshake if !bytes.HasPrefix(b, []byte("HTTP/1.1 101")) { firstLine := strings.SplitN(string(b), "\n", 1) log.Printf("[INFO] Websocket upgrade failed for %s: %s", r.URL, firstLine) http.Error(w, "websocket upgrade failed", http.StatusInternalServerError) return } out.SetReadDeadline(time.Time{}) errc := make(chan error, 2) cp := func(dst io.Writer, src io.Reader) { _, err := io.Copy(dst, src) errc <- err } go cp(out, in) go cp(in, out) err = <-errc if err != nil && err != io.EOF { log.Printf("[INFO] WS error for %s. %s", r.URL, err) } }) } ================================================ FILE: proxy/ws_integration_test.go ================================================ package proxy import ( "bytes" "io" "net/http" "net/http/httptest" "regexp" "strings" "testing" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/route" "golang.org/x/net/websocket" ) func TestProxyWSUpstream(t *testing.T) { wsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { case "/ws", "/wss", "/insecure", "/strip", "/foo/bar/baz", "/new/world": websocket.Handler(wsEchoHandler).ServeHTTP(w, r) default: w.WriteHeader(404) } })) defer wsServer.Close() t.Log("Started WS server: ", wsServer.URL) wssServer := httptest.NewUnstartedServer(websocket.Handler(wsEchoHandler)) wssServer.TLS = tlsServerConfig() wssServer.StartTLS() defer wssServer.Close() t.Log("Started WSS server: ", wssServer.URL) routes := "route add ws /ws " + wsServer.URL + "\n" routes += "route add ws /wss " + wssServer.URL + ` opts "proto=https"` + "\n" routes += "route add ws /insecure " + wssServer.URL + ` opts "proto=https tlsskipverify=true"` + "\n" routes += "route add ws /foo/strip " + wsServer.URL + ` opts "strip=/foo"` + "\n" routes += "route add ws /bar/baz " + wsServer.URL + ` opts "prepend=/foo"` + "\n" routes += "route add ws /old/world " + wsServer.URL + ` opts "prepend=/new strip=/old"` + "\n" httpProxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{NoRouteStatus: 404, GZIPContentTypes: regexp.MustCompile(".*")}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString(routes)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer httpProxy.Close() t.Log("Started HTTP proxy: ", httpProxy.URL) httpsProxy := httptest.NewUnstartedServer(&HTTPProxy{ Config: config.Proxy{NoRouteStatus: 404}, Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString(routes)) return tbl.Lookup(r, route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) httpsProxy.TLS = tlsServerConfig() httpsProxy.StartTLS() defer httpsProxy.Close() t.Log("Started HTTPS proxy: ", httpsProxy.URL) wsServerURL := wsServer.URL[len("http://"):] wssServerURL := wssServer.URL[len("https://"):] httpProxyURL := httpProxy.URL[len("http://"):] httpsProxyURL := httpsProxy.URL[len("https://"):] t.Run("ws-ws direct", func(t *testing.T) { testWSEcho(t, "ws://"+wsServerURL+"/ws", nil) }) t.Run("wss-wss direct", func(t *testing.T) { testWSEcho(t, "wss://"+wssServerURL+"/wss", nil) }) t.Run("ws-ws via http proxy", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/ws", nil) }) t.Run("wss-ws via https proxy", func(t *testing.T) { testWSEcho(t, "wss://"+httpsProxyURL+"/ws", nil) }) t.Run("ws-wss via http proxy", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/wss", nil) }) t.Run("wss-wss via https proxy", func(t *testing.T) { testWSEcho(t, "wss://"+httpsProxyURL+"/wss", nil) }) t.Run("ws-wss tlsskipverify=true via http proxy", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/insecure", nil) }) t.Run("wss-wss tlsskipverify=true via https proxy", func(t *testing.T) { testWSEcho(t, "wss://"+httpsProxyURL+"/insecure", nil) }) h := http.Header{"Accept-Encoding": []string{"gzip"}} t.Run("ws-ws via http proxy with gzip", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/ws", h) }) t.Run("ws-ws via http proxy with strip", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/foo/strip", nil) }) t.Run("ws-ws via http proxy with prepend", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/bar/baz", nil) }) t.Run("ws-ws via http proxy with strip & prepend", func(t *testing.T) { testWSEcho(t, "ws://"+httpProxyURL+"/old/world", nil) }) } func testWSEcho(t *testing.T, url string, hdr http.Header) { cfg, err := websocket.NewConfig(url, "http://localhost/") if err != nil { t.Fatal("NewConfig: ", err) } cfg.Header = hdr if strings.HasPrefix(url, "wss://") { cfg.TlsConfig = tlsClientConfig() } ws, err := websocket.DialConfig(cfg) if err != nil { t.Fatal(err) } defer ws.Close() send := []byte("foo") if _, err := ws.Write([]byte("foo")); err != nil { t.Logf("ws.Write failed: %s", err) } recv := make([]byte, 100) n, err := ws.Read(recv) if err != nil { t.Logf("ws.Read failed: %s", err) } recv = recv[:n] if got, want := recv, send; !bytes.Equal(got, want) { t.Fatalf("got %q want %q", got, want) } } func wsEchoHandler(ws *websocket.Conn) { io.Copy(ws, ws) } ================================================ FILE: registry/backend.go ================================================ package registry type Backend interface { // Register registers fabio as a service in the registry. Register(services []string) error // Deregister removes all service registrations for fabio. DeregisterAll() error // Deregister removes the given service registration for fabio. Deregister(service string) error // ManualPaths returns the list of paths for which there // are overrides. ManualPaths() ([]string, error) // ReadManual returns the current manual overrides and // their version as seen by the registry. ReadManual(path string) (value string, version uint64, err error) // WriteManual writes the new value to the registry if the // version of the stored document still matchhes version. WriteManual(path string, value string, version uint64) (ok bool, err error) // WatchServices watches the registry for changes in service // registration and health and pushes them if there is a difference. WatchServices() chan string // WatchManual watches the registry for changes in the manual // overrides and pushes them if there is a difference. WatchManual() chan string // WatchNoRouteHTML watches the registry for changes in the html returned // when a requested route is not found WatchNoRouteHTML() chan string } var Default Backend ================================================ FILE: registry/consul/backend.go ================================================ package consul import ( "errors" "log" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" "github.com/hashicorp/consul/api" ) // be is an implementation of a registry backend for consul. type be struct { c *api.Client cfg *config.Consul dereg map[string](chan bool) dc string } func NewBackend(cfg *config.Consul) (registry.Backend, error) { consulCfg := &api.Config{Address: cfg.Addr, Scheme: cfg.Scheme, Token: cfg.Token, Namespace: cfg.Namespace} if cfg.Scheme == "https" { consulCfg.TLSConfig.KeyFile = cfg.TLS.KeyFile consulCfg.TLSConfig.CertFile = cfg.TLS.CertFile consulCfg.TLSConfig.CAFile = cfg.TLS.CAFile consulCfg.TLSConfig.CAPath = cfg.TLS.CAPath consulCfg.TLSConfig.InsecureSkipVerify = cfg.TLS.InsecureSkipVerify } // create a reusable client c, err := api.NewClient(consulCfg) if err != nil { return nil, err } // ping the agent dc, err := datacenter(c) if err != nil { return nil, err } // we're good log.Printf("[INFO] consul: Connecting to %q in datacenter %q", cfg.Addr, dc) if cfg.Namespace != "" { log.Printf("[INFO] consul: Connecting to namespace %q", cfg.Namespace) } else { log.Printf("[INFO] consul: Connecting to default namespace") } return &be{c: c, dc: dc, cfg: cfg}, nil } func (b *be) Register(services []string) error { if b.dereg == nil { b.dereg = make(map[string](chan bool)) } if b.cfg.Register { services = append(services, b.cfg.ServiceName) } // deregister unneeded services for service := range b.dereg { if stringInSlice(service, services) { continue } err := b.Deregister(service) if err != nil { return err } } // register new services for _, service := range services { if b.dereg[service] != nil { log.Printf("[DEBUG] %q already registered", service) continue } serviceReg, err := serviceRegistration(b.cfg, service) if err != nil { return err } b.dereg[service] = register(b.c, serviceReg) } return nil } func (b *be) Deregister(service string) error { dereg := b.dereg[service] if dereg == nil { log.Printf("[WARN]: Attempted to deregister unknown service %q", service) return nil } dereg <- true // trigger deregistration <-dereg // wait for completion delete(b.dereg, service) return nil } func (b *be) DeregisterAll() error { log.Printf("[DEBUG]: consul: Deregistering all registered aliases.") for _, dereg := range b.dereg { if dereg == nil { continue } dereg <- true // trigger deregistration <-dereg // wait for completion } return nil } func (b *be) ManualPaths() ([]string, error) { keys, _, err := listKeys(b.c, b.cfg.KVPath, 0, b.cfg.RequireConsistent, b.cfg.AllowStale) return keys, err } func (b *be) ReadManual(path string) (value string, version uint64, err error) { // we cannot rely on the value provided by WatchManual() since // someone has to call that method first to kick off the go routine. return getKV(b.c, b.cfg.KVPath+path, 0, b.cfg.RequireConsistent, b.cfg.AllowStale) } func (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) { // try to create the key first by using version 0 if ok, err = putKV(b.c, b.cfg.KVPath+path, value, 0); ok { return } // then try the CAS update return putKV(b.c, b.cfg.KVPath+path, value, version) } func (b *be) WatchServices() chan string { log.Printf("[INFO] consul: Using dynamic routes") log.Printf("[INFO] consul: Using tag prefix %q", b.cfg.TagPrefix) m := NewServiceMonitor(b.c, b.cfg, b.dc) svc := make(chan string) go m.Watch(svc) return svc } func (b *be) WatchManual() chan string { log.Printf("[INFO] consul: Watching KV path %q", b.cfg.KVPath) kv := make(chan string) go watchKV(b.c, b.cfg.KVPath, kv, true, b.cfg.RequireConsistent, b.cfg.AllowStale) return kv } func (b *be) WatchNoRouteHTML() chan string { log.Printf("[INFO] consul: Watching KV path %q", b.cfg.NoRouteHTMLPath) html := make(chan string) go watchKV(b.c, b.cfg.NoRouteHTMLPath, html, false, b.cfg.RequireConsistent, b.cfg.AllowStale) return html } // datacenter returns the datacenter of the local agent func datacenter(c *api.Client) (string, error) { self, err := c.Agent().Self() if err != nil { return "", err } cfg, ok := self["Config"] if !ok { return "", errors.New("consul: self.Config not found") } dc, ok := cfg["Datacenter"].(string) if !ok { return "", errors.New("consul: self.Datacenter not found") } return dc, nil } func stringInSlice(str string, strSlice []string) bool { for _, s := range strSlice { if s == str { return true } } return false } ================================================ FILE: registry/consul/kv.go ================================================ package consul import ( "log" "strings" "time" "github.com/hashicorp/consul/api" ) // watchKV monitors a key in the KV store for changes. // The intended use case is to add additional route commands to the routing table. func watchKV(client *api.Client, path string, config chan string, separator bool, requireConsistent bool, allowStale bool) { var lastIndex uint64 var lastValue string for { value, index, err := listKV(client, path, lastIndex, separator, requireConsistent, allowStale) if err != nil { log.Printf("[WARN] consul: Error fetching config from %s. %v", path, err) time.Sleep(time.Second) continue } if value != lastValue || index != lastIndex { log.Printf("[DEBUG] consul: Manual config changed to #%d", index) config <- value lastValue, lastIndex = value, index } } } func listKeys(client *api.Client, path string, waitIndex uint64, requireConsistent bool, allowStale bool) ([]string, uint64, error) { q := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex} kvpairs, meta, err := client.KV().List(path, q) if err != nil { return nil, 0, err } if len(kvpairs) == 0 { return nil, meta.LastIndex, nil } var keys []string for _, kvpair := range kvpairs { keys = append(keys, kvpair.Key) } return keys, meta.LastIndex, nil } func listKV(client *api.Client, path string, waitIndex uint64, separator bool, requireConsistent bool, allowStale bool) (string, uint64, error) { q := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex} kvpairs, meta, err := client.KV().List(path, q) if err != nil { return "", 0, err } if len(kvpairs) == 0 { return "", meta.LastIndex, nil } var s []string for _, kvpair := range kvpairs { val := strings.TrimSpace(string(kvpair.Value)) if separator { val = "# --- " + kvpair.Key + "\n" + val } s = append(s, val) } return strings.Join(s, "\n\n"), meta.LastIndex, nil } func getKV(client *api.Client, key string, waitIndex uint64, requireConsistent bool, allowStale bool) (string, uint64, error) { q := &api.QueryOptions{RequireConsistent: requireConsistent, AllowStale: allowStale, WaitIndex: waitIndex} kvpair, meta, err := client.KV().Get(key, q) if err != nil { return "", 0, err } if kvpair == nil { return "", meta.LastIndex, nil } return strings.TrimSpace(string(kvpair.Value)), meta.LastIndex, nil } func putKV(client *api.Client, key, value string, index uint64) (bool, error) { p := &api.KVPair{Key: key[1:], Value: []byte(value), ModifyIndex: index} ok, _, err := client.KV().CAS(p, nil) if err != nil { return false, err } return ok, nil } ================================================ FILE: registry/consul/passing.go ================================================ package consul import ( "log" "strings" "github.com/hashicorp/consul/api" ) // passingServices takes a list of Consul Health Checks and only returns ones where the overall health of // the Service Instance is passing. This includes the health of the Node that the Services Instance runs on. func passingServices(checks []*api.HealthCheck, status []string, strict bool) []*api.HealthCheck { var p []*api.HealthCheck CHECKS: for _, svc := range checks { if !isServiceCheck(svc) { continue } var total, passing int for _, c := range checks { if svc.Node == c.Node { if svc.ServiceID == c.ServiceID { total++ if hasStatus(c, status) { passing++ } } if c.CheckID == "serfHealth" && c.Status == "critical" { log.Printf("[DEBUG] consul: Skipping service %q since agent on node %q is down: %s", c.ServiceID, c.Node, c.Output) continue CHECKS } if c.CheckID == "_node_maintenance" { log.Printf("[DEBUG] consul: Skipping service %q since node %q is in maintenance mode: %s", c.ServiceID, c.Node, c.Output) continue CHECKS } if c.CheckID == "_service_maintenance:"+svc.ServiceID && c.Status == "critical" { log.Printf("[DEBUG] consul: Skipping service %q since it is in maintenance mode: %s", svc.ServiceID, c.Output) continue CHECKS } } } if passing == 0 { continue } if strict && total != passing { continue } p = append(p, svc) } return p } // isServiceCheck returns true if the health check is a valid service check. func isServiceCheck(c *api.HealthCheck) bool { return c.ServiceID != "" && c.CheckID != "serfHealth" && c.CheckID != "_node_maintenance" && !strings.HasPrefix(c.CheckID, "_service_maintenance:") } // hasStatus returns true if the health check status is one of the given // values. func hasStatus(c *api.HealthCheck, status []string) bool { for _, s := range status { if c.Status == s { return true } } return false } ================================================ FILE: registry/consul/passing_test.go ================================================ package consul import ( "reflect" "testing" "github.com/hashicorp/consul/api" ) func TestPassingServices(t *testing.T) { var ( serfPass = &api.HealthCheck{Node: "node", CheckID: "serfHealth", Status: "passing"} serfFail = &api.HealthCheck{Node: "node", CheckID: "serfHealth", Status: "critical"} svc1Pass = &api.HealthCheck{Node: "node", CheckID: "service:abc", Status: "passing", ServiceName: "abc", ServiceID: "abc-1"} svc1Chk2Warn = &api.HealthCheck{Node: "node", CheckID: "service:abc", Status: "warning", ServiceName: "abc", ServiceID: "abc-1"} svc1Node2Pass = &api.HealthCheck{Node: "node2", CheckID: "service:abc", Status: "passing", ServiceName: "abc", ServiceID: "abc-1"} svc1Warn = &api.HealthCheck{Node: "node", CheckID: "service:abc", Status: "warning", ServiceName: "abc", ServiceID: "abc-2"} svc1Crit = &api.HealthCheck{Node: "node", CheckID: "service:abc", Status: "critical", ServiceName: "abc", ServiceID: "abc-3"} svc2Pass = &api.HealthCheck{Node: "node", CheckID: "my-check-id", Status: "passing", ServiceName: "def", ServiceID: "def-1"} svc1Maint = &api.HealthCheck{Node: "node", CheckID: "_service_maintenance:abc-1", Status: "critical", ServiceName: "abc", ServiceID: "abc-1"} svc1ID2Maint = &api.HealthCheck{Node: "node", CheckID: "_service_maintenance:abc-2", Status: "critical", ServiceName: "abc", ServiceID: "abc-2"} nodeMaint = &api.HealthCheck{Node: "node", CheckID: "_node_maintenance", Status: "critical"} ) tests := []struct { name string strict bool status []string in, out []*api.HealthCheck }{ { "expect no passing checks if checks array is nil", false, []string{"passing"}, nil, nil, }, { "expect no passing checks if checks array is empty", false, []string{"passing"}, []*api.HealthCheck{}, nil, }, { "expect check to pass if it has a matching status", false, []string{"passing"}, []*api.HealthCheck{svc1Pass}, []*api.HealthCheck{svc1Pass}, }, { "expect all checks to pass if they have a matching status", false, []string{"passing"}, []*api.HealthCheck{svc1Pass, svc2Pass}, []*api.HealthCheck{svc1Pass, svc2Pass}, }, { "expect that internal consul checks are filtered out", false, []string{"passing"}, []*api.HealthCheck{serfPass, svc1Pass}, []*api.HealthCheck{svc1Pass}, }, { "expect no passing checks if consul agent is unhealthy", false, []string{"passing"}, []*api.HealthCheck{serfFail, svc1Pass}, nil, }, { "expect no passing checks if node is in maintenance mode", false, []string{"passing"}, []*api.HealthCheck{nodeMaint, svc1Pass}, nil, }, { "expect no passing check if corresponding service is in maintenance mode", false, []string{"passing"}, []*api.HealthCheck{svc1Maint, svc1Pass}, nil, }, { "expect no passing check if node and service are in maintenance mode", false, []string{"passing"}, []*api.HealthCheck{nodeMaint, svc1Maint, svc1Pass}, nil, }, { "expect no passing check if agent is unhealthy or node and service are in maintenance mode", false, []string{"passing"}, []*api.HealthCheck{serfFail, nodeMaint, svc1Maint, svc1Pass}, nil, }, { "expect check of service which is not in maintenance mode to pass if another instance of same service is in maintenance mode", false, []string{"passing"}, []*api.HealthCheck{svc1ID2Maint, svc1Pass}, []*api.HealthCheck{svc1Pass}, }, { "expect that no checks of a service which is in maintenance mode are returned even if it has a passing check", false, []string{"passing"}, []*api.HealthCheck{svc1Maint, svc1Pass, svc2Pass}, []*api.HealthCheck{svc2Pass}, }, { "expect that a service's failing check does not affect a healthy instance of same service running on different node", false, []string{"passing"}, []*api.HealthCheck{svc1Crit, svc1Node2Pass}, []*api.HealthCheck{svc1Node2Pass}, }, { "service in maintenance mode does not affect healthy service running on different node", false, []string{"passing"}, []*api.HealthCheck{svc1Maint, svc1Node2Pass}, []*api.HealthCheck{svc1Node2Pass}, }, { "expect that internal consul check and failing check are not returned", false, []string{"passing", "warning"}, []*api.HealthCheck{serfPass, svc1Pass, svc1Crit}, []*api.HealthCheck{svc1Pass}, }, { "expect that internal consul check is filtered out and check with warning is passing", false, []string{"passing", "warning"}, []*api.HealthCheck{serfPass, svc1Warn, svc1Crit}, []*api.HealthCheck{svc1Warn}, }, { "expect that warning and passing non-internal checks are returned", false, []string{"passing", "warning"}, []*api.HealthCheck{serfPass, svc1Pass, svc1Warn}, []*api.HealthCheck{svc1Pass, svc1Warn}, }, { "expect that warning und passing non-internal checks are returned", false, []string{"passing", "warning"}, []*api.HealthCheck{serfPass, svc1Warn, svc1Crit, svc1Pass}, []*api.HealthCheck{svc1Warn, svc1Pass}, }, { "in non-strict mode, expect that checks which belong to same service are passing, if at least one of them is passing", false, []string{"passing"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, }, { "in strict mode, expect that no checks which belong to same service are passing, if not all of them are passing", true, []string{"passing"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, nil, }, { "in strict mode, expect that a failing check of one service does not affect a different service's passing check", true, []string{"passing"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn, svc2Pass}, []*api.HealthCheck{svc2Pass}, }, { "in strict mode, expect a check to pass if all of the other checks that belong to the same service are passing", true, []string{"passing", "warning"}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, []*api.HealthCheck{svc1Pass, svc1Chk2Warn}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got, want := passingServices(tt.in, tt.status, tt.strict), tt.out; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } } ================================================ FILE: registry/consul/register.go ================================================ package consul import ( "errors" "fmt" "log" "net" "os" "strconv" "strings" "time" "github.com/fabiolb/fabio/config" "github.com/hashicorp/consul/api" ) const ( TTLInterval = time.Second * 15 TTLRefreshInterval = time.Second * 10 TTLDeregisterCriticalServiceAfter = time.Minute ) // register keeps a service registered in consul. // // When a value is sent in the dereg channel the service is deregistered from // consul. To wait for completion the caller should read the next value from // the dereg channel. // // dereg <- true // trigger deregistration // <-dereg // wait for completion func register(c *api.Client, service *api.AgentServiceRegistration) chan bool { registered := func(serviceID string) bool { if serviceID == "" { return false } services, err := c.Agent().Services() if err != nil { log.Printf("[ERROR] consul: Cannot get service list. %s", err) return false } return services[serviceID] != nil } register := func() string { if err := c.Agent().ServiceRegister(service); err != nil { log.Printf("[ERROR] consul: Cannot register fabio [name:%q] in Consul. %s", service.Name, err) return "" } log.Printf("[INFO] consul: Registered fabio as %q", service.Name) log.Printf("[INFO] consul: Registered fabio with id %q", service.ID) log.Printf("[INFO] consul: Registered fabio with address %q", service.Address) log.Printf("[INFO] consul: Registered fabio with tags %q", strings.Join(service.Tags, ",")) for _, check := range service.Checks { log.Printf("[INFO] consul: Registered fabio with check %+v", check) } return service.ID } deregister := func(serviceID string) { log.Printf("[INFO] consul: Deregistering %q", service.Name) c.Agent().ServiceDeregister(serviceID) } passTTL := func(serviceTTLID string) { c.Agent().UpdateTTL(serviceTTLID, "", api.HealthPassing) } dereg := make(chan bool) go func() { var serviceID string var serviceTTLCheckId string for { if !registered(serviceID) { serviceID = register() serviceTTLCheckId = computeServiceTTLCheckId(serviceID) // Pass the TTL check right now so traffic can be served immediately. passTTL(serviceTTLCheckId) } select { case <-dereg: deregister(serviceID) dereg <- true return case <-time.After(TTLRefreshInterval): // Reset the TTL check clock. passTTL(serviceTTLCheckId) } } }() return dereg } func serviceRegistration(cfg *config.Consul, serviceName string) (*api.AgentServiceRegistration, error) { hostname, err := os.Hostname() if err != nil { return nil, err } ipstr, portstr, err := net.SplitHostPort(cfg.ServiceAddr) if err != nil { return nil, err } port, err := strconv.Atoi(portstr) if err != nil { return nil, err } ip := net.ParseIP(ipstr) if ip == nil { ip, err = config.LocalIP() if err != nil { return nil, err } if ip == nil { return nil, errors.New("no local ip") } } serviceID := fmt.Sprintf("%s-%s-%d", serviceName, hostname, port) checkURL := fmt.Sprintf("%s://%s:%d/health", cfg.CheckScheme, ip, port) if ip.To4() == nil { checkURL = fmt.Sprintf("%s://[%s]:%d/health", cfg.CheckScheme, ip, port) } service := &api.AgentServiceRegistration{ ID: serviceID, Name: serviceName, Address: ip.String(), Port: port, Tags: cfg.ServiceTags, Namespace: cfg.Namespace, // Set the checks for the service. // // Both checks must pass for Consul to consider the service healthy and therefore serve the fabio instance to clients. Checks: []*api.AgentServiceCheck{ // If fabio doesn't exit cleanly, it doesn't auto-deregister itself // from Consul. In order to address this, we introduce a TTL check // to confirm that the fabio instance is alive and able to route // this service. // // The TTL check must be refreshed before its timeout is crossed, // otherwise the check fails. If the check fails, Consul considers // this service to have become unhealthy. If the check is failing // (critical) for the DeregisterCriticalServiceAfter duration, the // Consul reaper will remove it from Consul. // // For more info, read https://www.consul.io/api/agent/check.html#deregistercriticalserviceafter. { CheckID: computeServiceTTLCheckId(serviceID), TTL: TTLInterval.String(), DeregisterCriticalServiceAfter: TTLDeregisterCriticalServiceAfter.String(), }, // HTTP check is meant to confirm fabio health endpoint is // reachable from the Consul agent. If the check fails, Consul // considers this service to have become unhealthy. { HTTP: checkURL, Interval: cfg.CheckInterval.String(), Timeout: cfg.CheckTimeout.String(), TLSSkipVerify: cfg.CheckTLSSkipVerify, }, }, } return service, nil } func computeServiceTTLCheckId(serviceID string) string { return serviceID + "-ttl" } ================================================ FILE: registry/consul/routecmd.go ================================================ package consul import ( "fmt" "log" "net" "os" "runtime" "strconv" "strings" "github.com/hashicorp/consul/api" ) // routecmd builds a route command. type routecmd struct { // svc is the consul service instance. svc *api.CatalogService env map[string]string // prefix is the prefix of urlprefix tags. e.g. 'urlprefix-'. prefix string } func (r routecmd) build() []string { var svctags, routetags []string for _, t := range r.svc.ServiceTags { t = strings.TrimSpace(t) if strings.HasPrefix(t, r.prefix) { routetags = append(routetags, t) } else { svctags = append(svctags, t) } } // generate route commands var config []string for _, tag := range routetags { if route, opts, ok := parseURLPrefixTag(tag, r.prefix, r.env); ok { name, addr, port := r.svc.ServiceName, r.svc.ServiceAddress, r.svc.ServicePort // use consul node address if service address is not set if addr == "" { addr = r.svc.Address } // add .local suffix on OSX for simple host names w/o domain if runtime.GOOS == "darwin" && !strings.Contains(addr, ".") && !strings.HasSuffix(addr, ".local") { addr += ".local" } addr = net.JoinHostPort(addr, strconv.Itoa(port)) //tags := strings.Join(r.tags, ",") dst := "http://" + addr + "/" var weight string var ropts []string for _, o := range strings.Fields(opts) { switch { case o == "proto=tcp": dst = "tcp://" + addr case o == "proto=https": dst = "https://" + addr case o == "proto=grpcs": dst = "grpcs://" + addr case o == "proto=grpc": dst = "grpc://" + addr case strings.HasPrefix(o, "weight="): weight = o[len("weight="):] case strings.HasPrefix(o, "redirect="): redir := strings.Split(o[len("redirect="):], ",") if len(redir) == 2 { dst = redir[1] ropts = append(ropts, fmt.Sprintf("redirect=%s", redir[0])) } else { log.Printf("[ERROR] Invalid syntax for redirect: %s. should be redirect=,", o) continue } default: ropts = append(ropts, o) } } cfg := "route add " + name + " " + route + " " + dst if weight != "" { cfg += " weight " + weight } if len(svctags) > 0 { cfg += " tags " + strconv.Quote(strings.Join(svctags, ",")) } if len(ropts) > 0 { cfg += " opts " + strconv.Quote(strings.Join(ropts, " ")) } config = append(config, cfg) } } return config } // parseURLPrefixTag expects an input in the form of 'tag-host/path[ opts]' // and returns the lower cased host and the unaltered path if the // prefix matches the tag. func parseURLPrefixTag(s, prefix string, env map[string]string) (route, opts string, ok bool) { // expand $x or ${x} to env[x] or "" expand := func(s string) string { return os.Expand(s, func(x string) string { if env == nil { return "" } return env[x] }) } s = strings.TrimSpace(s) if !strings.HasPrefix(s, prefix) { return "", "", false } s = strings.TrimSpace(s[len(prefix):]) p := strings.SplitN(s, " ", 2) if len(p) == 2 { opts = p[1] } s = p[0] // prefix is ":port" if strings.HasPrefix(s, ":") { return s, opts, true } if !strings.Contains(s, "/") { return s, opts, true } // prefix is "host/path" p = strings.SplitN(s, "/", 2) if len(p) == 1 { log.Printf("[WARN] consul: Invalid %s tag %q - You need to have a trailing slash!", prefix, s) return "", "", false } host, path := p[0], p[1] return strings.ToLower(expand(host)) + "/" + expand(path), opts, true } ================================================ FILE: registry/consul/routecmd_test.go ================================================ package consul import ( "reflect" "testing" "github.com/hashicorp/consul/api" ) func TestRouteCmd(t *testing.T) { cases := []struct { name string r routecmd cfg []string }{ { name: "http", r: routecmd{ prefix: "p-", svc: &api.CatalogService{ ServiceName: "svc-1", ServiceAddress: "1.1.1.1", ServicePort: 2222, ServiceTags: []string{`p-foo/bar`}, }, }, cfg: []string{ `route add svc-1 foo/bar http://1.1.1.1:2222/`, }, }, { name: "http single tag with a space", r: routecmd{ prefix: "p-", svc: &api.CatalogService{ ServiceName: "svc-1", ServiceAddress: "1.1.1.1", ServicePort: 2222, ServiceTags: []string{` p-foo/bar`}, }, }, cfg: []string{ `route add svc-1 foo/bar http://1.1.1.1:2222/`, }, }, { name: "http multiple tags", r: routecmd{ prefix: "p-", svc: &api.CatalogService{ ServiceName: "svc-1", ServiceAddress: "1.1.1.1", ServicePort: 2222, ServiceTags: []string{`p-foo/bar`, `p-test/foo`}, }, }, cfg: []string{ `route add svc-1 foo/bar http://1.1.1.1:2222/`, `route add svc-1 test/foo http://1.1.1.1:2222/`, }, }, { name: "http multiple routes with space prefix route", r: routecmd{ prefix: "p-", svc: &api.CatalogService{ ServiceName: "svc-1", ServiceAddress: "1.1.1.1", ServicePort: 2222, ServiceTags: []string{`p-foo/bar`, ` p-test/foo`}, }, }, cfg: []string{ `route add svc-1 foo/bar http://1.1.1.1:2222/`, `route add svc-1 test/foo http://1.1.1.1:2222/`, }, }, { name: "tcp", r: routecmd{ prefix: "p-", svc: &api.CatalogService{ ServiceName: "svc-1", ServiceAddress: "1.1.1.1", ServicePort: 2222, ServiceTags: []string{`p-:1234 proto=tcp`}, }, }, cfg: []string{ `route add svc-1 :1234 tcp://1.1.1.1:2222`, }, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got, want := c.r.build(), c.cfg; !reflect.DeepEqual(got, want) { t.Fatalf("\ngot %#v\nwant %#v", got, want) } }) } } func TestParseTag(t *testing.T) { prefix := "p-" tests := []struct { tag string env map[string]string route string opts string ok bool }{ {tag: "p", route: "", ok: false}, {tag: "p-", route: "", ok: true}, {tag: "p- ", route: "", ok: true}, {tag: "p-/", route: "/", ok: true}, {tag: " p-/", route: "/", ok: true}, {tag: "p-/ ", route: "/", ok: true}, {tag: "p- / ", route: "/", ok: true}, {tag: "p-/foo", route: "/foo", ok: true}, {tag: "p- /foo", route: "/foo", ok: true}, {tag: "p-1.1.1.1:999", route: "1.1.1.1:999", ok: true}, {tag: "p-bar/foo", route: "bar/foo", ok: true}, {tag: "p-bar/foo/foo", route: "bar/foo/foo", ok: true}, {tag: "p-www.bar.com/foo/foo", route: "www.bar.com/foo/foo", ok: true}, {tag: "p-WWW.BAR.COM/foo/foo", route: "www.bar.com/foo/foo", ok: true}, {tag: "p-bar/foo a b c", route: "bar/foo", opts: "a b c", ok: true}, { tag: "p-$x/$y", route: "/", ok: true, }, { tag: "p-${x}/${y}", route: "/", ok: true, }, { tag: "p-$x/$Y", env: map[string]string{"x": "Xx", "Y": "Yy"}, route: "xx/Yy", ok: true, }, { tag: "p-${x}/${Y}", env: map[string]string{"x": "Xx", "Y": "Yy"}, route: "xx/Yy", ok: true, }, { tag: "p-www.bar.com:80/foo redirect=302,https://www.bar.com", route: "www.bar.com:80/foo", opts: "redirect=302,https://www.bar.com", ok: true, }, } for i, tt := range tests { uri, opts, ok := parseURLPrefixTag(tt.tag, prefix, tt.env) if got, want := ok, tt.ok; got != want { t.Errorf("%d: got %v want %v", i, got, want) } if !ok { continue } if got, want := uri, tt.route; got != want { t.Errorf("%d: got uri %q want %q", i, got, want) } if got, want := opts, tt.opts; got != want { t.Errorf("%d: got opts %q want %q", i, got, want) } } } ================================================ FILE: registry/consul/service.go ================================================ package consul import ( "fmt" "log" "sort" "strings" "time" "github.com/fabiolb/fabio/config" "github.com/hashicorp/consul/api" ) // ServiceMonitor generates fabio configurations from consul state. type ServiceMonitor struct { client *api.Client config *config.Consul dc string strict bool } func NewServiceMonitor(client *api.Client, config *config.Consul, dc string) *ServiceMonitor { return &ServiceMonitor{ client: client, config: config, dc: dc, strict: config.ChecksRequired == "all", } } // Watch monitors the consul health checks and sends a new // configuration to the updates channel on every change. func (w *ServiceMonitor) Watch(updates chan string) { var lastIndex uint64 var q *api.QueryOptions for { if w.config.PollInterval != 0 { q = &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale} time.Sleep(w.config.PollInterval) } else { q = &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale, WaitIndex: lastIndex} } checks, meta, err := w.client.Health().State("any", q) if err != nil { log.Printf("[WARN] consul: Error fetching health state. %v", err) time.Sleep(time.Second) continue } log.Printf("[DEBUG] consul: Health changed to #%d", meta.LastIndex) prefixedChecks := checksWithTagPrefix(w.config.TagPrefix, checks) log.Printf("[DEBUG] consul: only %d of %d checks have the configured tag prefix", len(prefixedChecks), len(checks)) // determine which services have passing health checks passing := passingServices(prefixedChecks, w.config.ServiceStatus, w.strict) // build the config for the passing services updates <- w.makeConfig(passing) // remember the last state and wait for the next change lastIndex = meta.LastIndex } } // makeConfig determines which service instances have passing health checks // and then finds the ones which have tags with the right prefix to build the config from. func (w *ServiceMonitor) makeConfig(checks []*api.HealthCheck) string { // map service name to list of service passing for which the health check is ok m := map[string]map[string]bool{} for _, check := range checks { // Make the node part of the id, because according to the Consul docs // the ServiceID is unique per agent but not cluster wide // https://www.consul.io/api/agent/service.html#id name, id := check.ServiceName, fmt.Sprintf("%s.%s", check.Node, check.ServiceID) if _, ok := m[name]; !ok { m[name] = map[string]bool{} } m[name][id] = true } n := w.config.ServiceMonitors if n <= 0 { n = 1 } sem := make(chan int, n) cfgs := make(chan []string, len(m)) for name, passing := range m { name, passing := name, passing go func() { sem <- 1 cfgs <- w.serviceConfig(name, passing) <-sem }() } var config []string for range m { cfg := <-cfgs config = append(config, cfg...) } // sort config in reverse order to sort most specific config to the top sort.Sort(sort.Reverse(sort.StringSlice(config))) return strings.Join(config, "\n") } // serviceConfig constructs the config for all good instances of a single service. func (w *ServiceMonitor) serviceConfig(name string, passing map[string]bool) (config []string) { if name == "" || len(passing) == 0 { return nil } q := &api.QueryOptions{RequireConsistent: w.config.RequireConsistent, AllowStale: w.config.AllowStale} svcs, _, err := w.client.Catalog().Service(name, "", q) if err != nil { log.Printf("[WARN] consul: Error getting catalog service %s. %v", name, err) return nil } env := map[string]string{ "DC": w.dc, } for _, svc := range svcs { // check if this instance passed the health check if _, ok := passing[svc.Node+"."+svc.ServiceID]; !ok { continue } r := routecmd{ svc: svc, env: env, prefix: w.config.TagPrefix, } cmds := r.build() config = append(config, cmds...) } return config } // checksWithTagPrefix filters a list of Consul Health Checks to only the Checks with a Tag that begins with the prefix func checksWithTagPrefix(prefix string, checks api.HealthChecks) api.HealthChecks { checksWithPrefix := make(api.HealthChecks, 0, len(checks)) for _, c := range checks { if c.CheckID == "serfHealth" || c.CheckID == "_node_maintenance" || strings.HasPrefix(c.CheckID, "_service_maintenance") { checksWithPrefix = append(checksWithPrefix, c) continue } for _, t := range c.ServiceTags { if strings.HasPrefix(t, prefix) { checksWithPrefix = append(checksWithPrefix, c) break } } } return checksWithPrefix } ================================================ FILE: registry/custom/backend.go ================================================ package custom import ( "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" "log" ) type be struct { cfg *config.Custom } func NewBackend(cfg *config.Custom) (registry.Backend, error) { return &be{cfg}, nil } func (b *be) Register(services []string) error { return nil } func (b *be) Deregister(serviceName string) error { return nil } func (b *be) DeregisterAll() error { return nil } func (b *be) ManualPaths() ([]string, error) { return nil, nil } func (b *be) ReadManual(string) (value string, version uint64, err error) { return "", 0, nil } func (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) { return false, nil } func (b *be) WatchServices() chan string { log.Printf("[INFO] custom: Using custom routes from %s", b.cfg.Host) ch := make(chan string, 1) go customRoutes(b.cfg, ch) return ch } func (b *be) WatchManual() chan string { return make(chan string) } func (b *be) WatchNoRouteHTML() chan string { ch := make(chan string, 1) ch <- b.cfg.NoRouteHTML return ch } ================================================ FILE: registry/custom/custom.go ================================================ package custom import ( "crypto/tls" "encoding/json" "fmt" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/route" "log" "net/http" "time" ) func customRoutes(cfg *config.Custom, ch chan string) { var Routes *[]route.RouteDef var trans *http.Transport var URL string if !cfg.CheckTLSSkipVerify { trans = &http.Transport{} } else { trans = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } client := &http.Client{ Transport: trans, Timeout: cfg.Timeout, } if cfg.QueryParams != "" { URL = fmt.Sprintf("%s://%s/%s?%s", cfg.Scheme, cfg.Host, cfg.Path, cfg.QueryParams) } else { URL = fmt.Sprintf("%s://%s/%s", cfg.Scheme, cfg.Host, cfg.Path) } req, err := http.NewRequest("GET", URL, nil) if err != nil { log.Printf("[ERROR] Can not generate new HTTP request") } req.Close = true for { func() { log.Printf("[DEBUG] Custom Registry starting request %s \n", time.Now()) resp, err := client.Do(req) if resp != nil { defer func() { if err := resp.Body.Close(); err != nil { log.Printf("Error Can not close HTTP resp body - %s -%s \n", URL, err.Error()) } }() } if err != nil { ch <- fmt.Sprintf("Error Sending HTTPs Request To Custom be - %s -%s", URL, err.Error()) time.Sleep(cfg.PollInterval) return } if resp.StatusCode != 200 { ch <- fmt.Sprintf("Error Non-200 return (%v) from -%s", resp.StatusCode, URL) time.Sleep(cfg.PollInterval) return } log.Printf("[DEBUG] Custom Registry begin decoding json %s \n", time.Now()) decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&Routes) if err != nil { ch <- fmt.Sprintf("Error decoding request - %s -%s", URL, err.Error()) time.Sleep(cfg.PollInterval) return } log.Printf("[DEBUG] Custom Registry building table %s \n", time.Now()) t, err := route.NewTableCustom(Routes) if err != nil { ch <- fmt.Sprintf("Error generating new table - %s", err.Error()) } log.Printf("[DEBUG] Custom Registry building table complete %s \n", time.Now()) route.SetTable(t) log.Printf("[DEBUG] Custom Registry table set complete %s \n", time.Now()) ch <- "OK" time.Sleep(cfg.PollInterval) }() } } ================================================ FILE: registry/custom/custom_test.go ================================================ package custom import ( "encoding/json" "fmt" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/route" "net/http" "testing" "time" ) func TestCustomRoutes(t *testing.T) { var resp string cfg := config.Custom{ Host: "localhost:8080", Path: "test", Scheme: "http", CheckTLSSkipVerify: false, PollInterval: 3 * time.Second, Timeout: 3 * time.Second, } ch := make(chan string, 1) mux := http.NewServeMux() mux.HandleFunc("/test", handleTest) server := &http.Server{ Addr: "localhost:8080", Handler: mux, } go server.ListenAndServe() time.Sleep(3 * time.Second) defer server.Close() go customRoutes(&cfg, ch) resp = <-ch if resp != "OK" { fmt.Printf("Failed to get routes for custom backend - %s", resp) t.FailNow() } } func handleTest(w http.ResponseWriter, r *http.Request) { var routes []route.RouteDef var tags = []string{"tag1", "tag2"} var opts = make(map[string]string) opts["tlsskipverify"] = "true" opts["proto"] = "http" var route1 = route.RouteDef{ Cmd: "route add", Service: "service1", Src: "app.com", Dst: "http://10.1.1.1:8080", Weight: 0.50, Tags: tags, Opts: opts, } var route2 = route.RouteDef{ Cmd: "route add", Service: "service1", Src: "app.com", Dst: "http://10.1.1.2:8080", Weight: 0.50, Tags: tags, Opts: opts, } var route3 = route.RouteDef{ Cmd: "route add", Service: "service2", Src: "app.com", Dst: "http://10.1.1.3:8080", Weight: 0.25, Tags: tags, Opts: opts, } routes = append(routes, route1) routes = append(routes, route2) routes = append(routes, route3) rt, _ := json.Marshal(routes) w.Write(rt) } ================================================ FILE: registry/file/backend.go ================================================ // Package file implements a simple file based registry // backend which reads the routes from a file once. package file import ( "log" "os" "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" "github.com/fabiolb/fabio/registry/static" ) func NewBackend(cfg *config.File) (registry.Backend, error) { routes, err := os.ReadFile(cfg.RoutesPath) if err != nil { log.Println("[ERROR] Cannot read routes from ", cfg.RoutesPath) return nil, err } noroutehtml, err := os.ReadFile(cfg.NoRouteHTMLPath) if err != nil { log.Println("[ERROR] Cannot read no route HTML from ", cfg.NoRouteHTMLPath) return nil, err } staticCfg := &config.Static{ NoRouteHTML: string(noroutehtml), Routes: string(routes), } return static.NewBackend(staticCfg) } ================================================ FILE: registry/static/backend.go ================================================ // Package static implements a simple static registry // backend which uses statically configured routes. package static import ( "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" ) type be struct { cfg *config.Static } func NewBackend(cfg *config.Static) (registry.Backend, error) { return &be{cfg}, nil } func (b *be) Register(services []string) error { return nil } func (b *be) Deregister(serviceName string) error { return nil } func (b *be) DeregisterAll() error { return nil } func (b *be) ManualPaths() ([]string, error) { return nil, nil } func (b *be) ReadManual(string) (value string, version uint64, err error) { return "", 0, nil } func (b *be) WriteManual(path string, value string, version uint64) (ok bool, err error) { return false, nil } func (b *be) WatchServices() chan string { ch := make(chan string, 1) ch <- b.cfg.Routes return ch } func (b *be) WatchManual() chan string { return make(chan string) } func (b *be) WatchNoRouteHTML() chan string { ch := make(chan string, 1) ch <- b.cfg.NoRouteHTML return ch } ================================================ FILE: rootwarn_unix.go ================================================ //go:build !windows // +build !windows package main import ( "log" "os" "sync" "time" ) const interval = time.Hour const warnInsecure = ` ************************************************************ You are running fabio as root with the '-insecure' flag Please check https://fabiolb.net/faq/binding-to-low-ports/ for alternatives. ************************************************************ ` const warn17behavior = ` ************************************************************ You are running fabio as root without the '-insecure' flag This will stop working with fabio 1.7! ************************************************************ ` var once sync.Once func WarnIfRunAsRoot(allowRoot bool) { // todo(fs): should we emit the same warning when running inside a container? // todo(fs): check for existence of `/.dockerenv` to determine Docker environment. isRoot := os.Getuid() == 0 if !isRoot { return } doWarn(allowRoot) once.Do(func() { go remind(allowRoot) }) } func doWarn(allowRoot bool) { warn := warnInsecure if !allowRoot { warn = warn17behavior } log.Printf("[INFO] Running fabio as UID=%d EUID=%d GID=%d", os.Getuid(), os.Geteuid(), os.Getgid()) log.Print("[WARN] ", warn) } func remind(allowRoot bool) { for { doWarn(allowRoot) time.Sleep(interval) } } ================================================ FILE: rootwarn_windows.go ================================================ //go:build windows // +build windows package main func WarnIfRunAsRoot(allowRoot bool) { // windows not supported } ================================================ FILE: route/access_rules.go ================================================ package route import ( "errors" "fmt" "log" "net" "net/http" "strings" ) const ( ipAllowTag = "allow:ip" ipDenyTag = "deny:ip" ) // AccessDeniedHTTP checks rules on the target for HTTP proxy routes. func (t *Target) AccessDeniedHTTP(r *http.Request) bool { // No rules ... skip checks if len(t.accessRules) == 0 { return false } host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { log.Printf("[ERROR] failed to get host from remote header %s: %s", r.RemoteAddr, err.Error()) return false } ip := net.ParseIP(host) if ip == nil { log.Printf("[WARN] failed to parse remote address %s", host) } // check remote source and return if denied if t.denyByIP(ip) { return true } // check xff source if present if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Trusting XFF headers sent from clients is dangerous and generally // bad practice. Therefore, we cannot assume which if any of the elements // is the actual client address. To try and avoid the chance of spoofed // headers and/or loose upstream proxies we validate all elements in the header. // Specifically AWS does not strip XFF from anonymous internet sources: // https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-for // See lengthy github discussion for more background: https://github.com/fabiolb/fabio/pull/449 for _, xip := range strings.Split(xff, ",") { xip = strings.TrimSpace(xip) if xip == host { continue } if ip = net.ParseIP(xip); ip == nil { log.Printf("[WARN] failed to parse xff address %s", xip) continue } if t.denyByIP(ip) { return true } } } // default allow return false } // AccessDeniedTCP checks rules on the target for TCP proxy routes. func (t *Target) AccessDeniedTCP(c net.Conn) bool { // Calling RemoteAddr on a proxy-protocol enabled connection may block. // Therefore we explicitly check and bail out early if there are no // rules defined for the target. // See https://github.com/fabiolb/fabio/issues/524 for background. if len(t.accessRules) == 0 { return false } // get remote address and validate assertion addr, ok := c.RemoteAddr().(*net.TCPAddr) if !ok { log.Printf("[ERROR] failed to assert remote connection address for %s", t.Service) return false } // check remote connection address if t.denyByIP(addr.IP) { return true } // default allow return false } func (t *Target) denyByIP(ip net.IP) bool { if ip == nil || len(t.accessRules) == 0 { return false } // check allow (whitelist) first if it exists if _, ok := t.accessRules[ipAllowTag]; ok { var block *net.IPNet for _, x := range t.accessRules[ipAllowTag] { if block, ok = x.(*net.IPNet); !ok { log.Printf("[ERROR] failed to assert ip block while checking allow rule for %s", t.Service) continue } // debug logging log.Printf("[DEBUG] checking %s against ip allow rule %s", ip.String(), block.String()) // check block if block.Contains(ip) { // debug logging log.Printf("[DEBUG] allowing request from %s via %s", ip.String(), block.String()) // specific allow matched - allow this request return false } } // we checked all the blocks - deny this request log.Printf("[INFO] route rules denied access from %s to %s", ip.String(), t.URL.String()) return true } // still going - check deny (blacklist) if it exists if _, ok := t.accessRules[ipDenyTag]; ok { var block *net.IPNet for _, x := range t.accessRules[ipDenyTag] { if block, ok = x.(*net.IPNet); !ok { log.Printf("[INFO] failed to assert ip block while checking deny rule for %s", t.Service) continue } // debug logging log.Printf("[DEBUG] checking %s against ip deny rule %s", ip.String(), block.String()) // check block if block.Contains(ip) { // specific deny matched - deny this request log.Printf("[INFO] route rules denied access from %s to %s", ip.String(), t.URL.String()) return true } } } // debug logging log.Printf("[DEBUG] default allowing request from %s that was not denied", ip.String()) // default - do not deny return false } // ProcessAccessRules processes access rules from options specified on the target route func (t *Target) ProcessAccessRules() error { if t.Opts["allow"] != "" && t.Opts["deny"] != "" { return errors.New("specifying allow and deny on the same route is not supported") } for _, allowDeny := range []string{"allow", "deny"} { if t.Opts[allowDeny] != "" { if err := t.parseAccessRule(allowDeny); err != nil { return err } } } return nil } func (t *Target) parseAccessRule(allowDeny string) error { var accessTag string var temps []string var value string var ip net.IP // init rules if needed if t.accessRules == nil { t.accessRules = make(map[string][]interface{}) } // loop over rule elements for _, c := range strings.Split(t.Opts[allowDeny], ",") { if temps = strings.SplitN(c, ":", 2); len(temps) != 2 { return fmt.Errorf("invalid access item, expected :, got %s", temps) } // form access type tag accessTag = allowDeny + ":" + strings.ToLower(strings.TrimSpace(temps[0])) // switch on formed access tag - currently only ip types are implemented switch accessTag { case ipAllowTag, ipDenyTag: if value = strings.TrimSpace(temps[1]); !strings.Contains(value, "/") { if ip = net.ParseIP(value); ip == nil { return fmt.Errorf("failed to parse IP %s", value) } if ip.To4() != nil { value = ip.String() + "/32" } else { value = ip.String() + "/128" } } _, net, err := net.ParseCIDR(value) if err != nil { return fmt.Errorf("failed to parse CIDR %s with error: %s", c, err.Error()) } // add element to rule map t.accessRules[accessTag] = append(t.accessRules[accessTag], net) default: return fmt.Errorf("unknown access item type: %s", temps[0]) } } return nil } ================================================ FILE: route/access_rules_test.go ================================================ package route import ( "net" "net/http" "net/url" "testing" ) func TestAccessRules_parseAccessRule(t *testing.T) { tests := []struct { desc string allowDeny string fail bool }{ { desc: "valid ipv4 rule", allowDeny: "ip:10.0.0.0/8,ip:192.168.0.0/24,ip:1.2.3.4/32", }, { desc: "valid ipv6 rule", allowDeny: "ip:1234:567:beef:cafe::/64,ip:1234:5678:dead:beef::/32", }, { desc: "invalid rule type", allowDeny: "xxx:10.0.0.0/8", fail: true, }, { desc: "ip rule with incomplete address", allowDeny: "ip:10/8", fail: true, }, { desc: "ip rule with bad cidr mask", allowDeny: "ip:10.0.0.0/255", fail: true, }, { desc: "single ipv4 with no mask", allowDeny: "ip:1.2.3.4", fail: false, }, { desc: "single ipv6 with no mask", allowDeny: "ip:fe80::1", fail: false, }, } for i, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { for _, ad := range []string{"allow", "deny"} { tgt := &Target{Opts: map[string]string{ad: tt.allowDeny}} err := tgt.parseAccessRule(ad) if err != nil && !tt.fail { t.Errorf("%d: %s\nfailed: %s", i, tt.desc, err.Error()) return } } }) } } func TestAccessRules_denyByIP(t *testing.T) { tests := []struct { desc string target *Target remote net.IP denied bool }{ { desc: "allow rule with included ipv4", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, remote: net.ParseIP("10.10.0.1"), denied: false, }, { desc: "allow rule with exluded ipv4", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, remote: net.ParseIP("1.2.3.4"), denied: true, }, { desc: "deny rule with included ipv4", target: &Target{ Opts: map[string]string{"deny": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, remote: net.ParseIP("10.10.0.1"), denied: true, }, { desc: "deny rule with excluded ipv4", target: &Target{ Opts: map[string]string{"deny": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, remote: net.ParseIP("1.2.3.4"), denied: false, }, { desc: "allow rule with included ipv6", target: &Target{ Opts: map[string]string{"allow": "ip:1234:dead:beef:cafe::/64"}, }, remote: net.ParseIP("1234:dead:beef:cafe::5678"), denied: false, }, { desc: "allow rule with exluded ipv6", target: &Target{ Opts: map[string]string{"allow": "ip:1234:dead:beef:cafe::/64"}, }, remote: net.ParseIP("1234:5678::1"), denied: true, }, { desc: "deny rule with included ipv6", target: &Target{ Opts: map[string]string{"deny": "ip:1234:dead:beef:cafe::/64"}, }, remote: net.ParseIP("1234:dead:beef:cafe::5678"), denied: true, }, { desc: "deny rule with excluded ipv6", target: &Target{ Opts: map[string]string{"deny": "ip:1234:dead:beef:cafe::/64"}, }, remote: net.ParseIP("1234:5678::1"), denied: false, }, } for i, tt := range tests { tt := tt // capture loop var t.Run(tt.desc, func(t *testing.T) { if err := tt.target.ProcessAccessRules(); err != nil { t.Errorf("%d: %s - failed to process access rules: %s", i, tt.desc, err.Error()) } tt.target.URL, _ = url.Parse("http://testing.test/") if deny := tt.target.denyByIP(tt.remote); deny != tt.denied { t.Errorf("%d: %s\ngot denied: %t\nwant denied: %t\n", i, tt.desc, deny, tt.denied) return } }) } } func TestAccessRules_AccessDeniedHTTP(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com/", nil) tests := []struct { desc string target *Target xff string remote string denied bool }{ { desc: "single denied xff and allowed remote addr", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, xff: "10.11.12.13, 1.1.1.2, 10.11.12.14", remote: "10.11.12.1:65500", denied: true, }, { desc: "allowed xff and denied remote addr", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, xff: "10.11.12.13, 1.2.3.4", remote: "1.1.1.2:65500", denied: true, }, { desc: "single allowed xff and allowed remote addr", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, xff: "10.11.12.13, 1.2.3.4", remote: "192.168.0.12:65500", denied: true, }, { desc: "denied xff and denied remote addr", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, xff: "1.2.3.4, 10.11.12.13, 10.11.12.14", remote: "200.17.18.20:65500", denied: true, }, { desc: "all allowed xff and allowed remote addr", target: &Target{ Opts: map[string]string{"allow": "ip:10.0.0.0/8,ip:192.168.0.0/24"}, }, xff: "10.11.12.13, 10.110.120.130", remote: "192.168.0.12:65500", denied: false, }, } for i, tt := range tests { tt := tt // capture loop var req.Header = http.Header{"X-Forwarded-For": []string{tt.xff}} req.RemoteAddr = tt.remote t.Run(tt.desc, func(t *testing.T) { if err := tt.target.ProcessAccessRules(); err != nil { t.Errorf("%d: %s - failed to process access rules: %s", i, tt.desc, err.Error()) } tt.target.URL = mustParse("http://testing.test/") if deny := tt.target.AccessDeniedHTTP(req); deny != tt.denied { t.Errorf("%d: %s\ngot denied: %t\nwant denied: %t\n", i, tt.desc, deny, tt.denied) return } }) } } ================================================ FILE: route/auth.go ================================================ package route import ( "log" "net/http" "github.com/fabiolb/fabio/auth" ) func (t *Target) Authorized(r *http.Request, w http.ResponseWriter, authSchemes map[string]auth.AuthScheme) bool { if t.AuthScheme == "" { return true } scheme := authSchemes[t.AuthScheme] if scheme == nil { log.Printf("[ERROR] unknown auth scheme '%s'\n", t.AuthScheme) return false } return scheme.Authorized(r, w) } ================================================ FILE: route/auth_test.go ================================================ package route import ( "net/http" "reflect" "testing" "github.com/fabiolb/fabio/auth" ) type testAuth struct { ok bool } func (t *testAuth) Authorized(r *http.Request, w http.ResponseWriter) bool { return t.ok } type responseWriter struct { header http.Header code int written []byte } func (rw *responseWriter) Header() http.Header { return rw.header } func (rw *responseWriter) Write(b []byte) (int, error) { rw.written = append(rw.written, b...) return len(rw.written), nil } func (rw *responseWriter) WriteHeader(statusCode int) { rw.code = statusCode } func TestTarget_Authorized(t *testing.T) { tests := []struct { name string authScheme string authSchemes map[string]auth.AuthScheme out bool }{ { name: "matches correct auth scheme", authScheme: "mybasic", authSchemes: map[string]auth.AuthScheme{ "mybasic": &testAuth{ok: true}, }, out: true, }, { name: "returns true when scheme is empty", authScheme: "", authSchemes: map[string]auth.AuthScheme{ "mybasic": &testAuth{ok: false}, }, out: true, }, { name: "returns false when scheme is unknown", authScheme: "foobar", authSchemes: map[string]auth.AuthScheme{ "mybasic": &testAuth{ok: true}, }, out: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { target := &Target{ AuthScheme: tt.authScheme, } if got, want := target.Authorized(&http.Request{}, &responseWriter{}, tt.authSchemes), tt.out; !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } } ================================================ FILE: route/glob_cache.go ================================================ package route import ( "github.com/gobwas/glob" "sync" ) // GlobCache implements an LRU cache for compiled glob patterns. type GlobCache struct { // m maps patterns to compiled glob matchers. m sync.Map // l contains the added patterns and serves as an LRU cache. // l has a fixed size and is initialized in the constructor. l []string // h is the first element in l. h int // n is the number of elements in l. n int } func NewGlobCache(size int) *GlobCache { return &GlobCache{ l: make([]string, size), } } // Get returns the compiled glob pattern if it compiled without // error. Otherwise, the function returns nil. If the pattern // is not in the cache it will be added. func (c *GlobCache) Get(pattern string) (glob.Glob, error) { // fast path if glb, ok := c.m.Load(pattern); ok { //Type Assert the returned interface{} return glb.(glob.Glob), nil } // try to compile pattern glbCompiled, err := glob.Compile(pattern) if err != nil { return nil, err } // if the LRU buffer is not full just append // the element to the buffer. if c.n < len(c.l) { c.m.Store(pattern, glbCompiled) c.l[c.n] = pattern c.n++ return glbCompiled, nil } // otherwise, remove the oldest element and move // the head. Note that once the buffer is full // (c.n == len(c.l)) it will never become smaller // again. // TODO add logging for cache full - How will this impact performance c.m.Delete(c.l[c.h]) c.m.Store(pattern, glbCompiled) c.l[c.h] = pattern c.h = (c.h + 1) % c.n return glbCompiled, nil } ================================================ FILE: route/glob_cache_test.go ================================================ package route import ( "reflect" "sort" "testing" ) func TestGlobCache(t *testing.T) { c := NewGlobCache(3) keys := func() []string { var kk []string c.m.Range(func(k, v interface{}) bool { kk = append(kk, k.(string)) return true }) sort.Strings(kk) return kk } c.Get("a") // TODO add back in when sync.Map supports Len function // TODO https://github.com/golang/go/issues/20680 //if got, want := len(c.m), 1; got != want { // t.Fatalf("got len %d want %d", got, want) //} if got, want := keys(), []string{"a"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } if got, want := c.l, []string{"a", "", ""}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } c.Get("b") // TODO add back in when sync.Map supports Len function // TODO https://github.com/golang/go/issues/20680 //if got, want := len(c.m), 2; got != want { // t.Fatalf("got len %d want %d", got, want) //} if got, want := keys(), []string{"a", "b"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } if got, want := c.l, []string{"a", "b", ""}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } c.Get("c") // TODO add back in when sync.Map supports Len function // TODO https://github.com/golang/go/issues/20680 //if got, want := len(c.m), 3; got != want { // t.Fatalf("got len %d want %d", got, want) //} if got, want := keys(), []string{"a", "b", "c"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } if got, want := c.l, []string{"a", "b", "c"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } c.Get("d") // TODO add back in when sync.Map supports Len function // TODO https://github.com/golang/go/issues/20680 //if got, want := len(c.m), 3; got != want { // t.Fatalf("got len %d want %d", got, want) //} if got, want := keys(), []string{"b", "c", "d"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } if got, want := c.l, []string{"d", "b", "c"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } } ================================================ FILE: route/issue57_test.go ================================================ package route import ( "bytes" "net/http" "testing" ) // TestIssue57 tests that after deleting a all targets for // a route requests to that route are handled by the next // matching route. func TestIssue57(t *testing.T) { tests := []string{ ` route add svca / http://foo.com:800 route add svcb /foo http://foo.com:900 route del svcb /foo http://foo.com:900`, ` route add svca / http://foo.com:800 route add svcb /foo http://foo.com:900 route del svcb /foo`, ` route add svca / http://foo.com:800 route add svcb /foo http://foo.com:900 route del svcb`, } req := &http.Request{URL: mustParse("/foo")} want := "http://foo.com:800" for i, tt := range tests { tbl, err := NewTable(bytes.NewBufferString(tt)) if err != nil { t.Fatalf("%d: got %v want nil", i, err) } target := tbl.Lookup(req, rrPicker, prefixMatcher, globCache, globEnabled) if target == nil { t.Fatalf("%d: got %v want %v", i, target, want) } if got := target.URL.String(); got != want { t.Errorf("%d: got %v want %v", i, got, want) } } } ================================================ FILE: route/matcher.go ================================================ package route import ( "strings" ) // matcher determines whether a host/path matches a route type matcher func(uri string, r *Route) bool // Matcher contains the available matcher functions. // Update config/load.go#load after updating. var Matcher = map[string]matcher{ "prefix": prefixMatcher, "glob": globMatcher, "iprefix": iPrefixMatcher, } // prefixMatcher matches path to the routes' path. func prefixMatcher(uri string, r *Route) bool { return strings.HasPrefix(uri, r.Path) } // globMatcher matches path to the routes' path using gobwas/glob. func globMatcher(uri string, r *Route) bool { return r.Glob.Match(uri) } // iPrefixMatcher matches path to the routes' path ignoring case func iPrefixMatcher(uri string, r *Route) bool { // todo(fs): if this turns out to be a performance issue we should cache // todo(fs): strings.ToLower(r.Path) in r.PathLower lowerURI := strings.ToLower(uri) lowerPath := strings.ToLower(r.Path) return strings.HasPrefix(lowerURI, lowerPath) } ================================================ FILE: route/matcher_test.go ================================================ package route import ( "testing" "github.com/gobwas/glob" ) func TestPrefixMatcher(t *testing.T) { tests := []struct { uri string matches bool route *Route }{ {uri: "/foo", matches: true, route: &Route{Path: "/foo"}}, {uri: "/fools", matches: true, route: &Route{Path: "/foo"}}, {uri: "/fo", matches: false, route: &Route{Path: "/foo"}}, {uri: "/bar", matches: false, route: &Route{Path: "/foo"}}, } for _, tt := range tests { t.Run(tt.uri, func(t *testing.T) { if got, want := prefixMatcher(tt.uri, tt.route), tt.matches; got != want { t.Fatalf("got %v want %v", got, want) } }) } } func TestGlobMatcher(t *testing.T) { tests := []struct { uri string matches bool route *Route }{ // happy flows {uri: "/foo", matches: true, route: &Route{Path: "/foo"}}, {uri: "/fool", matches: true, route: &Route{Path: "/foo?"}}, {uri: "/fool", matches: true, route: &Route{Path: "/foo*"}}, {uri: "/fools", matches: true, route: &Route{Path: "/foo*"}}, {uri: "/fools", matches: true, route: &Route{Path: "/foo*"}}, {uri: "/foo/x/bar", matches: true, route: &Route{Path: "/foo/*/bar"}}, {uri: "/foo/x/y/z/w/bar", matches: true, route: &Route{Path: "/foo/**"}}, {uri: "/foo/x/y/z/w/bar", matches: true, route: &Route{Path: "/foo/**/bar"}}, // error flows {uri: "/fo", matches: false, route: &Route{Path: "/foo"}}, {uri: "/fools", matches: false, route: &Route{Path: "/foo"}}, {uri: "/fo", matches: false, route: &Route{Path: "/foo*"}}, {uri: "/fools", matches: false, route: &Route{Path: "/foo.*"}}, {uri: "/foo/x/y/z/w/baz", matches: false, route: &Route{Path: "/foo/**/bar"}}, } for _, tt := range tests { t.Run(tt.uri, func(t *testing.T) { tt.route.Glob = glob.MustCompile(tt.route.Path) if got, want := globMatcher(tt.uri, tt.route), tt.matches; got != want { t.Fatalf("got %v want %v", got, want) } }) } } func TestIPrefixMatcher(t *testing.T) { tests := []struct { uri string matches bool route *Route }{ {uri: "/foo", matches: false, route: &Route{Path: "/fool"}}, {uri: "/foo", matches: true, route: &Route{Path: "/foo"}}, {uri: "/Fool", matches: true, route: &Route{Path: "/foo"}}, {uri: "/foo", matches: true, route: &Route{Path: "/Foo"}}, } for _, tt := range tests { t.Run(tt.uri, func(t *testing.T) { if got, want := iPrefixMatcher(tt.uri, tt.route), tt.matches; got != want { t.Fatalf("got %v want %v", got, want) } }) } } ================================================ FILE: route/metrics_cleanup_test.go ================================================ package route import ( "bytes" "strings" "testing" "github.com/fabiolb/fabio/metrics" gkm "github.com/go-kit/kit/metrics" "github.com/prometheus/client_golang/prometheus" ) // TestMetricsCleanup verifies that stale metrics are cleaned up when routes are removed func TestMetricsCleanup(t *testing.T) { // Create a custom prometheus registry for this test reg := prometheus.NewRegistry() // Create histogram and counter vecs hv := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "fabio", Name: "route", Help: "test", Buckets: prometheus.DefBuckets, }, []string{"service", "host", "path", "target"}) reg.MustRegister(hv) rxCv := prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "fabio", Name: "route_rx_total", Help: "test", }, []string{"service", "host", "path", "target"}) reg.MustRegister(rxCv) txCv := prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "fabio", Name: "route_tx_total", Help: "test", }, []string{"service", "host", "path", "target"}) reg.MustRegister(txCv) // Create a test provider that wraps our vecs counters.histogram = &testDeletableHistogram{hv: hv} counters.rxCounter = &testDeletableCounter{cv: rxCv} counters.txCounter = &testDeletableCounter{cv: txCv} // Create initial table with two services t1, err := NewTable(bytes.NewBufferString(` route add svc-a /path-a http://target-a:8080/ route add svc-b /path-b http://target-b:8080/ `)) if err != nil { t.Fatalf("Failed to create table 1: %v", err) } // Store it SetTable(t1) // Simulate traffic to create metric series for _, routes := range t1 { for _, r := range routes { for _, target := range r.Targets { target.Timer.Observe(0.1) target.RxCounter.Add(100) target.TxCounter.Add(200) } } } // Check that metrics exist for both services mfs, _ := reg.Gather() t.Log("Metrics after initial traffic:") svcAFound, svcBFound := false, false for _, mf := range mfs { for _, m := range mf.GetMetric() { var labels []string for _, l := range m.GetLabel() { labels = append(labels, l.GetName()+"="+l.GetValue()) if l.GetValue() == "svc-a" { svcAFound = true } if l.GetValue() == "svc-b" { svcBFound = true } } t.Logf(" %s{%s}", mf.GetName(), strings.Join(labels, ", ")) } } if !svcAFound { t.Error("svc-a metrics not found after initial traffic") } if !svcBFound { t.Error("svc-b metrics not found after initial traffic") } // Now create a new table WITHOUT svc-a t2, err := NewTable(bytes.NewBufferString(` route add svc-b /path-b http://target-b:8080/ `)) if err != nil { t.Fatalf("Failed to create table 2: %v", err) } t.Logf("Old table keys: %v", collectTableMetricKeys(t1)) t.Logf("New table keys: %v", collectTableMetricKeys(t2)) // Set the new table - this should trigger cleanup SetTable(t2) // Check that svc-a metrics are gone mfs, _ = reg.Gather() t.Log("Metrics after removing svc-a:") svcAFound, svcBFound = false, false for _, mf := range mfs { for _, m := range mf.GetMetric() { var labels []string for _, l := range m.GetLabel() { labels = append(labels, l.GetName()+"="+l.GetValue()) if l.GetValue() == "svc-a" { svcAFound = true } if l.GetValue() == "svc-b" { svcBFound = true } } t.Logf(" %s{%s}", mf.GetName(), strings.Join(labels, ", ")) } } if svcAFound { t.Error("svc-a metrics should have been cleaned up but were still found") } if !svcBFound { t.Error("svc-b metrics should still exist but were not found") } } // testDeletableHistogram wraps HistogramVec for testing type testDeletableHistogram struct { hv *prometheus.HistogramVec lvs []string } func (h *testDeletableHistogram) Observe(v float64) { h.hv.WithLabelValues(extractValues(h.lvs)...).Observe(v) } func (h *testDeletableHistogram) With(labelValues ...string) gkm.Histogram { return &testDeletableHistogram{ hv: h.hv, lvs: append(append([]string{}, h.lvs...), labelValues...), } } func (h *testDeletableHistogram) DeleteLabelValues(labelValues ...string) bool { return h.hv.DeleteLabelValues(labelValues...) } // testDeletableCounter wraps CounterVec for testing type testDeletableCounter struct { cv *prometheus.CounterVec lvs []string } func (c *testDeletableCounter) Add(v float64) { c.cv.WithLabelValues(extractValues(c.lvs)...).Add(v) } func (c *testDeletableCounter) With(labelValues ...string) gkm.Counter { return &testDeletableCounter{ cv: c.cv, lvs: append(append([]string{}, c.lvs...), labelValues...), } } func (c *testDeletableCounter) DeleteLabelValues(labelValues ...string) bool { return c.cv.DeleteLabelValues(labelValues...) } // extractValues extracts only values from alternating key-value pairs func extractValues(lvs []string) []string { vals := make([]string, 0, len(lvs)/2) for i := 1; i < len(lvs); i += 2 { vals = append(vals, lvs[i]) } return vals } // Verify interfaces are satisfied var _ metrics.DeletableHistogram = (*testDeletableHistogram)(nil) var _ metrics.DeletableCounter = (*testDeletableCounter)(nil) // TestMetricsCleanupViaMultiProvider tests the cleanup through the MultiProvider // which is the actual code path used in production. func TestMetricsCleanupViaMultiProvider(t *testing.T) { // Create a custom prometheus registry for this test reg := prometheus.NewRegistry() // Create a test provider that mimics the real PromProvider testProv := &testPromProvider{reg: reg} // Wrap it in a MultiProvider like production does multiProv := metrics.NewMultiProvider([]metrics.Provider{testProv}) // Set up the route package to use this provider SetMetricsProvider(multiProv) // Verify that the histogram is a MultiHistogram _, ok := counters.histogram.(*metrics.MultiHistogram) if !ok { t.Fatalf("Expected MultiHistogram, got %T", counters.histogram) } // Also verify it implements DeletableHistogram _, ok = counters.histogram.(metrics.DeletableHistogram) if !ok { t.Fatal("MultiHistogram should implement DeletableHistogram") } // Create initial table with two services t1, err := NewTable(bytes.NewBufferString(` route add svc-a /path-a http://target-a:8080/ route add svc-b /path-b http://target-b:8080/ `)) if err != nil { t.Fatalf("Failed to create table 1: %v", err) } SetTable(t1) // Simulate traffic to create metric series for _, routes := range t1 { for _, r := range routes { for _, target := range r.Targets { target.Timer.Observe(0.1) } } } // Verify svc-a metrics exist mfs, _ := reg.Gather() svcAFound := false for _, mf := range mfs { for _, m := range mf.GetMetric() { for _, l := range m.GetLabel() { if l.GetValue() == "svc-a" { svcAFound = true } } } } if !svcAFound { t.Error("svc-a metrics should exist after traffic") } // Remove svc-a from the table t2, err := NewTable(bytes.NewBufferString(` route add svc-b /path-b http://target-b:8080/ `)) if err != nil { t.Fatalf("Failed to create table 2: %v", err) } SetTable(t2) // Verify svc-a metrics are gone mfs, _ = reg.Gather() svcAFound = false for _, mf := range mfs { for _, m := range mf.GetMetric() { for _, l := range m.GetLabel() { if l.GetValue() == "svc-a" { svcAFound = true t.Logf("Found unexpected metric: %s with label %s=%s", mf.GetName(), l.GetName(), l.GetValue()) } } } } if svcAFound { t.Error("svc-a metrics should have been cleaned up via MultiProvider") } } // testPromProvider is a test provider that creates deletable metrics type testPromProvider struct { reg *prometheus.Registry } func (p *testPromProvider) NewCounter(name string, labels ...string) gkm.Counter { cv := prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "test", Name: sanitizeName(name), }, labels) p.reg.MustRegister(cv) return &testDeletableCounter{cv: cv} } func (p *testPromProvider) NewGauge(name string, labels ...string) gkm.Gauge { gv := prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "test", Name: sanitizeName(name), }, labels) p.reg.MustRegister(gv) return &testDeletableGauge{gv: gv} } func (p *testPromProvider) NewHistogram(name string, labels ...string) gkm.Histogram { hv := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "test", Name: sanitizeName(name), Buckets: prometheus.DefBuckets, }, labels) p.reg.MustRegister(hv) return &testDeletableHistogram{hv: hv} } // sanitizeName replaces dots with underscores for prometheus metric names func sanitizeName(name string) string { return strings.ReplaceAll(name, ".", "_") } // testDeletableGauge for completeness type testDeletableGauge struct { gv *prometheus.GaugeVec lvs []string } func (g *testDeletableGauge) Set(v float64) { g.gv.WithLabelValues(extractValues(g.lvs)...).Set(v) } func (g *testDeletableGauge) Add(v float64) { g.gv.WithLabelValues(extractValues(g.lvs)...).Add(v) } func (g *testDeletableGauge) With(labelValues ...string) gkm.Gauge { return &testDeletableGauge{ gv: g.gv, lvs: append(append([]string{}, g.lvs...), labelValues...), } } func (g *testDeletableGauge) DeleteLabelValues(labelValues ...string) bool { return g.gv.DeleteLabelValues(labelValues...) } var _ metrics.DeletableGauge = (*testDeletableGauge)(nil) var _ metrics.Provider = (*testPromProvider)(nil) ================================================ FILE: route/parse_new.go ================================================ package route import ( "bufio" "bytes" "errors" "fmt" "regexp" "strconv" "strings" ) var ( reRouteAdd = regexp.MustCompile(`^route\s+add`) reRouteDel = regexp.MustCompile(`^route\s+del`) reRouteWeight = regexp.MustCompile(`^route\s+weight`) reComment = regexp.MustCompile(`^(#|//)`) reBlankLine = regexp.MustCompile(`^\s*$`) ) const Commands = ` Route commands can have the following form: route add [ weight ][ tags ",,..."][ opts "k1=v1 k2=v2 ..."] - Add route for service svc from src to dst with optional weight, tags and options. Valid options are: strip=/path : forward '/path/to/file' as '/to/file' prepend=/prefix : forward '/path/to/file' as '/prefix/path/to/file' proto=tcp : upstream service is TCP, dst is ':port' proto=https : upstream service is HTTPS tlsskipverify=true : disable TLS cert validation for HTTPS upstream host=name : set the Host header to 'name'. If 'name == "dst"' then the 'Host' header will be set to the registered upstream host name register=name : register fabio as new service 'name'. Useful for registering hostnames for host specific routes. auth=name : name of the auth scheme to use (defined in proxy.auth) route del [ [ ]] - Remove route matching svc, src and/or dst route del tags ",,..." - Remove all routes of service matching svc and tags route del tags ",,..." - Remove all routes matching tags route weight weight tags ",,..." - Route w% of traffic to all services matching svc, src and tags route weight weight tags ",,..." - Route w% of traffic to all services matching src and tags route weight weight - Route w% of traffic to all services matching svc and src route weight service host/path weight w tags "tag1,tag2" - Route w% of traffic to all services matching service, host/path and tags w is a float > 0 describing a percentage, e.g. 0.5 == 50% w <= 0: means no fixed weighting. Traffic is evenly distributed w > 0: route will receive n% of traffic. If sum(w) > 1 then w is normalized. sum(w) >= 1: only matching services will receive traffic Note that the total sum of traffic sent to all matching routes is w%. ` // Parse loads a routing table from a set of route commands. // // The commands are parsed in order and order matters. // Deleting a route that has not been created yet yields // a different result than the other way around. func Parse(in *bytes.Buffer) (defs []*RouteDef, err error) { var def *RouteDef var i int scanner := bufio.NewScanner(in) for scanner.Scan() { var err error result := strings.TrimSpace(scanner.Text()) i++ switch { case reComment.MatchString(result) || reBlankLine.MatchString(result): continue case reRouteAdd.MatchString(result): def, err = parseRouteAdd(result) case reRouteDel.MatchString(result): def, err = parseRouteDel(result) case reRouteWeight.MatchString(result): def, err = parseRouteWeight(result) default: err = errors.New("syntax error: 'route' expected") } if err != nil { return nil, fmt.Errorf("line %d: %s", i, err) } defs = append(defs, def) } return defs, nil } // ParseAliases scans a set of route commands for the "register" option and // returns a list of services which should be registered by the backend. func ParseAliases(in string) (names []string, err error) { var defs []*RouteDef var def *RouteDef for i, s := range strings.Split(in, "\n") { var err error s = strings.TrimSpace(s) switch { case reComment.MatchString(s) || reBlankLine.MatchString(s): continue case reRouteAdd.MatchString(s): def, err = parseRouteAdd(s) case reRouteDel.MatchString(s): def, err = parseRouteDel(s) case reRouteWeight.MatchString(s): def, err = parseRouteWeight(s) default: err = errors.New("syntax error: 'route' expected") } if err != nil { return nil, fmt.Errorf("line %d: %s", i+1, err) } defs = append(defs, def) } var aliases []string for _, d := range defs { registerName, ok := d.Opts["register"] if ok { aliases = append(aliases, registerName) } } return aliases, nil } // route add [ weight ][ tags ",,..."][ opts "k=v k=v ..."] // 1: service 2: src 3: dst 4: weight expr 5: weight val 6: tags expr 7: tags val 8: opts expr 9: opts val var reAdd = mustCompileWithFlexibleSpace(`^route add (\S+) (\S+) (\S+)( weight (\S+))?( tags "([^"]*)")?( opts "([^"]*)")?$`) func parseRouteAdd(s string) (*RouteDef, error) { if m := reAdd.FindStringSubmatch(s); m != nil { w, err := parseWeight(m[5]) return &RouteDef{ Cmd: RouteAddCmd, Service: m[1], Src: m[2], Dst: m[3], Weight: w, Tags: parseTags(m[7]), Opts: parseOpts(m[9]), }, err } return nil, errors.New("syntax error: 'route add' invalid") } // route del [ ][ ] // 1: service 2: src expr 3: src 4: dst expr 5: dst var reDel = mustCompileWithFlexibleSpace(`^route del (\S+)( (\S+)( (\S+))?)?$`) // route del tags ",,..." // 1: service 2: tags var reDelSvcTags = mustCompileWithFlexibleSpace(`^route del (\S+) tags "([^"]*)"$`) // route del tags ",,..." // 2: tags var reDelTags = mustCompileWithFlexibleSpace(`^route del tags "([^"]*)"$`) func parseRouteDel(s string) (*RouteDef, error) { if m := reDelSvcTags.FindStringSubmatch(s); m != nil { return &RouteDef{Cmd: RouteDelCmd, Service: m[1], Tags: parseTags(m[2])}, nil } if m := reDelTags.FindStringSubmatch(s); m != nil { return &RouteDef{Cmd: RouteDelCmd, Tags: parseTags(m[1])}, nil } if m := reDel.FindStringSubmatch(s); m != nil { return &RouteDef{Cmd: RouteDelCmd, Service: m[1], Src: m[3], Dst: m[5]}, nil } return nil, errors.New("syntax error: 'route del' invalid") } // route weight weight [ tags ",,..."] // 1: service 2: src 3: weight val 4: tags expr 5: tags val var reWeightSvc = mustCompileWithFlexibleSpace(`^route weight (\S+) (\S+) weight (\S+)( tags "([^"]*)")?$`) // route weight weight tags ",,..." // 1: src 2: weight val 3: tags val var reWeightSrc = mustCompileWithFlexibleSpace(`^route weight (\S+) weight (\S+) tags "([^"]*)"$`) func parseRouteWeight(s string) (*RouteDef, error) { if m := reWeightSvc.FindStringSubmatch(s); m != nil { w, err := parseWeight(m[3]) return &RouteDef{ Cmd: RouteWeightCmd, Service: m[1], Src: m[2], Weight: w, Tags: parseTags(m[5]), }, err } if m := reWeightSrc.FindStringSubmatch(s); m != nil { w, err := parseWeight(m[2]) return &RouteDef{ Cmd: RouteWeightCmd, Src: m[1], Weight: w, Tags: parseTags(m[3]), }, err } return nil, errors.New("syntax error: 'route weight' invalid") } func mustCompileWithFlexibleSpace(re string) *regexp.Regexp { return regexp.MustCompile(strings.ReplaceAll(re, " ", "\\s+")) } func parseWeight(s string) (float64, error) { if s == "" { return 0, nil } f, err := strconv.ParseFloat(s, 64) if err != nil { return 0.0, errors.New("syntax error: weight value invalid") } return f, nil } func parseTags(s string) []string { if s == "" { return nil } tags := strings.Split(s, ",") for i, t := range tags { tags[i] = strings.TrimSpace(t) } return tags } func parseOpts(s string) map[string]string { if s == "" { return nil } m := make(map[string]string) for _, f := range strings.Fields(s) { p := strings.SplitN(f, "=", 2) if len(p) == 1 { m[f] = "" } else { m[p[0]] = p[1] } } return m } ================================================ FILE: route/parse_test.go ================================================ package route import ( "bytes" "reflect" "regexp" "testing" ) func TestParse(t *testing.T) { tests := []struct { desc string in string out []*RouteDef fail bool }{ // error flows {"FailEmpty", ``, nil, false}, {"FailNoRoute", `bang`, nil, true}, {"FailRouteNoCmd", `route x`, nil, true}, {"FailRouteAddNoService", `route add`, nil, true}, {"FailRouteAddNoSrc", `route add svc`, nil, true}, // happy flows { desc: "RouteAddService", in: `route add svc /prefix http://1.2.3.4/`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/"}}, }, { desc: "RouteAddTCPService", in: `route add svc :1234 tcp://1.2.3.4:5678`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: ":1234", Dst: "tcp://1.2.3.4:5678"}}, }, { desc: "RouteAddGRPCService", in: `route add svc :1234 grpc://1.2.3.4:5678`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: ":1234", Dst: "grpc://1.2.3.4:5678"}}, }, { desc: "RouteAddServiceWeight", in: `route add svc /prefix http://1.2.3.4/ weight 1.2`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/", Weight: 1.2}}, }, { desc: "RouteAddServiceWeightTags", in: `route add svc /prefix http://1.2.3.4/ weight 1.2 tags "a,b"`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/", Weight: 1.2, Tags: []string{"a", "b"}}}, }, { desc: "RouteAddServiceWeightOpts", in: `route add svc /prefix http://1.2.3.4/ weight 1.2 opts "foo=bar baz=bang blimp"`, out: []*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": ""}}}, }, { desc: "RouteAddServiceWeightTagsOpts", in: `route add svc /prefix http://1.2.3.4/ weight 1.2 tags "a,b" opts "foo=bar baz=bang blimp"`, out: []*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": ""}}}, }, { desc: "RouteAddServiceWeightTagsOptsMoreSpaces", in: ` route add svc /prefix http://1.2.3.4/ weight 1.2 tags " a , b " opts "foo=bar baz=bang blimp" `, out: []*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": ""}}}, }, { desc: "RouteAddTags", in: `route add svc /prefix http://1.2.3.4/ tags "a,b"`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/", Tags: []string{"a", "b"}}}, }, { desc: "RouteAddTagsOpts", in: `route add svc /prefix http://1.2.3.4/ tags "a,b" opts "foo=bar baz=bang blimp"`, out: []*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": ""}}}, }, { desc: "RouteAddOpts", in: `route add svc /prefix http://1.2.3.4/ opts "foo=bar baz=bang blimp"`, out: []*RouteDef{{Cmd: RouteAddCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/", Opts: map[string]string{"foo": "bar", "baz": "bang", "blimp": ""}}}, }, { desc: "RouteDelTags", in: `route del tags "a,b"`, out: []*RouteDef{{Cmd: RouteDelCmd, Tags: []string{"a", "b"}}}, }, { desc: "RouteDelTagsMoreSpaces", in: `route del tags " a , b "`, out: []*RouteDef{{Cmd: RouteDelCmd, Tags: []string{"a", "b"}}}, }, { desc: "RouteDelService", in: `route del svc`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc"}}, }, { desc: "RouteDelServiceTags", in: `route del svc tags "a,b"`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Tags: []string{"a", "b"}}}, }, { desc: "RouteDelServiceTagsMoreSpaces", in: `route del svc tags " a , b "`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Tags: []string{"a", "b"}}}, }, { desc: "RouteDelServiceSrc", in: `route del svc /prefix`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Src: "/prefix"}}, }, { desc: "RouteDelTCPServiceSrc", in: `route del svc :1234`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Src: ":1234"}}, }, { desc: "RouteDelServiceSrcDst", in: `route del svc /prefix http://1.2.3.4/`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/"}}, }, { desc: "RouteDelTCPServiceSrcDst", in: `route del svc :1234 tcp://1.2.3.4:5678`, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Src: ":1234", Dst: "tcp://1.2.3.4:5678"}}, }, { desc: "RouteDelServiceSrcDstMoreSpaces", in: ` route del svc /prefix http://1.2.3.4/ `, out: []*RouteDef{{Cmd: RouteDelCmd, Service: "svc", Src: "/prefix", Dst: "http://1.2.3.4/"}}, }, { desc: "RouteWeightServiceSrc", in: `route weight svc /prefix weight 1.2`, out: []*RouteDef{{Cmd: RouteWeightCmd, Service: "svc", Src: "/prefix", Weight: 1.2}}, }, { desc: "RouteWeightServiceSrcTags", in: `route weight svc /prefix weight 1.2 tags "a,b"`, out: []*RouteDef{{Cmd: RouteWeightCmd, Service: "svc", Src: "/prefix", Weight: 1.2, Tags: []string{"a", "b"}}}, }, { desc: "RouteWeightServiceSrcTagsMoreSpaces", in: ` route weight svc /prefix weight 1.2 tags " a , b " `, out: []*RouteDef{{Cmd: RouteWeightCmd, Service: "svc", Src: "/prefix", Weight: 1.2, Tags: []string{"a", "b"}}}, }, { desc: "RouteWeightSrcTags", in: `route weight /prefix weight 1.2 tags "a,b"`, out: []*RouteDef{{Cmd: RouteWeightCmd, Src: "/prefix", Weight: 1.2, Tags: []string{"a", "b"}}}, }, { desc: "RouteWeightSrcTagsMoreSpaces", in: ` route weight /prefix weight 1.2 tags " a , b " `, out: []*RouteDef{{Cmd: RouteWeightCmd, Src: "/prefix", Weight: 1.2, Tags: []string{"a", "b"}}}, }, } reSyntaxError := regexp.MustCompile(`syntax error`) deref := func(def []*RouteDef) (defs []RouteDef) { for _, d := range def { defs = append(defs, *d) } return } run := func(in string, def []*RouteDef, fail bool, parseFn func(*bytes.Buffer) ([]*RouteDef, error)) { out, err := parseFn(bytes.NewBufferString(in)) switch { case err == nil && fail: t.Errorf("got error nil want fail") return case err != nil && !fail: t.Errorf("got error %v want nil", err) return case err != nil: if !reSyntaxError.MatchString(err.Error()) { t.Errorf("got error %q want 'syntax error.*'", err) } return } if got, want := out, def; !reflect.DeepEqual(got, want) { t.Errorf("\ngot %#v\nwant %#v", deref(got), deref(want)) } } for _, tt := range tests { t.Run("Parse-"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, Parse) }) } } func TestParseAliases(t *testing.T) { tests := []struct { desc string in string out []string fail bool }{ // error flows {"FailEmpty", ``, nil, false}, {"FailNoRoute", `bang`, nil, true}, {"FailRouteNoCmd", `route x`, nil, true}, {"FailRouteAddNoService", `route add`, nil, true}, {"FailRouteAddNoSrc", `route add svc`, nil, true}, // happy flows with and without aliases { desc: "RouteAddServiceWithoutAlias", in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path prepend=/new proto=https"`, out: []string(nil), }, { desc: "RouteAddServiceWithAlias", in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path prepend=/new proto=https register=alpha"`, out: []string{"alpha"}, }, { desc: "RouteAddServicesWithoutAliases", in: `route add alpha-be alpha/ http://1.2.3.4/ opts "strip=/path prepend=/new proto=tcp" route add bravo-be bravo/ http://1.2.3.5/ route add charlie-be charlie/ http://1.2.3.6/ opts "host=charlie"`, out: []string(nil), }, { desc: "RouteAddServicesWithAliases", in: `route add alpha-be alpha/ http://1.2.3.4/ opts "register=alpha" route add bravo-be bravo/ http://1.2.3.5/ opts "strip=/foo prepend=/new register=bravo" route add charlie-be charlie/ http://1.2.3.5/ opts "host=charlie proto=https" route add delta-be delta/ http://1.2.3.5/ opts "host=delta proto=https register=delta"`, out: []string{"alpha", "bravo", "delta"}, }, } reSyntaxError := regexp.MustCompile(`syntax error`) run := func(in string, aliases []string, fail bool, parseFn func(string) ([]string, error)) { out, err := parseFn(in) switch { case err == nil && fail: t.Errorf("got error nil want fail") return case err != nil && !fail: t.Errorf("got error %v want nil", err) return case err != nil: if !reSyntaxError.MatchString(err.Error()) { t.Errorf("got error %q want 'syntax error.*'", err) } return } if got, want := out, aliases; !reflect.DeepEqual(got, want) { t.Errorf("\ngot %#v\nwant %#v", got, want) } } for _, tt := range tests { t.Run("ParseAliases-"+tt.desc, func(t *testing.T) { run(tt.in, tt.out, tt.fail, ParseAliases) }) } } ================================================ FILE: route/picker.go ================================================ package route import ( "math/rand" "sync/atomic" ) // picker selects a target from a list of targets. type picker func(r *Route) *Target // Picker contains the available picker functions. // Update config/load.go#load after updating. var Picker = map[string]picker{ "rnd": rndPicker, "rr": rrPicker, } // rndPicker picks a random target from the list of targets. func rndPicker(r *Route) *Target { return r.wTargets[randIntn(len(r.wTargets))] } // rrPicker picks the next target from a list of targets using round-robin. func rrPicker(r *Route) *Target { u := r.wTargets[r.total%uint64(len(r.wTargets))] atomic.AddUint64(&r.total, 1) return u } // as it turns out, math/rand's Intn is now way faster (4x) than the previous implementation using // time.UnixNano(). As a bonus, this actually works properly on 32 bit platforms. var randIntn = func(n int) int { if n == 0 { return 0 } return rand.Intn(n) } ================================================ FILE: route/picker_test.go ================================================ package route import ( "net/url" "reflect" "testing" "time" ) var ( fooDotCom = mustParse("http://foo.com/") barDotCom = mustParse("http://bar.com/") ) func mustParse(rawurl string) *url.URL { u, err := url.Parse(rawurl) if err != nil { panic(err) } return u } func TestRndPicker(t *testing.T) { r := &Route{Host: "www.bar.com", Path: "/foo"} r.addTarget("svc", fooDotCom, 0, nil, nil) r.addTarget("svc", barDotCom, 0, nil, nil) tests := []struct { rnd int targetURL *url.URL }{ {0, fooDotCom}, {1, barDotCom}, } prev := randIntn defer func() { randIntn = prev }() for i, tt := range tests { randIntn = func(int) int { return i } if got, want := rndPicker(r).URL, tt.targetURL; !reflect.DeepEqual(got, want) { t.Errorf("%d: got %v want %v", i, got, want) } } } func TestRRPicker(t *testing.T) { r := &Route{Host: "www.bar.com", Path: "/foo"} r.addTarget("svc", fooDotCom, 0, nil, nil) r.addTarget("svc", barDotCom, 0, nil, nil) tests := []*url.URL{fooDotCom, barDotCom, fooDotCom, barDotCom, fooDotCom, barDotCom} for i, tt := range tests { if got, want := rrPicker(r).URL, tt; !reflect.DeepEqual(got, want) { t.Errorf("%d: got %v want %v", i, got, want) } } } // This is an improved version of the previous UnixNano implementation // This one does not overflow on 32 bit platforms, it casts to int after // doing mod. doing it before caused overflows. var oldRandInt = func(n int) int { if n == 0 { return 0 } return int(time.Now().UnixNano() / int64(time.Microsecond) % int64(n)) } var result int // prevent compiler optimization func BenchmarkOldRandIntn(b *testing.B) { var r int // more shields against compiler optimization for i := range b.N { r = oldRandInt(i) } result = r } func BenchmarkMathRandIntn(b *testing.B) { var r int // more shields against compiler optimization for i := range b.N { r = randIntn(i) } result = r } ================================================ FILE: route/route.go ================================================ package route import ( "crypto/tls" "fmt" "github.com/fabiolb/fabio/transport" "log" "net/url" "reflect" "sort" "strconv" "strings" "github.com/gobwas/glob" ) // Route maps a path prefix to one or more target URLs. // routes can have a weight value which describes the // amount of traffic this route should get. You can specify // that a route should get a fixed percentage of the traffic // independent of how many instances are running. type Route struct { // Glob represents compiled pattern. Glob glob.Glob // Host contains the host of the route. // not used for routing but for config generation // Table has a map with the host as key // for faster lookup and smaller search space. Host string // Path is the path prefix from a request uri Path string // Targets contains the list of URLs Targets []*Target // wTargets contains targets distributed according to their weight wTargets []*Target // total contains the total number of requests for this route. // Used by the RRPicker total uint64 } func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float64, tags []string, opts map[string]string) { if fixedWeight < 0 { fixedWeight = 0 } // de-dup existing target for _, t := range r.Targets { if t.Service == service && t.URL.String() == targetURL.String() && t.FixedWeight == fixedWeight && reflect.DeepEqual(t.Tags, tags) { return } } t := &Target{ Service: service, Tags: tags, Opts: opts, URL: targetURL, FixedWeight: fixedWeight, Timer: counters.histogram.With("service", service, "host", r.Host, "path", r.Path, "target", targetURL.String()), RxCounter: counters.rxCounter.With("service", service, "host", r.Host, "path", r.Path, "target", targetURL.String()), TxCounter: counters.txCounter.With("service", service, "host", r.Host, "path", r.Path, "target", targetURL.String()), } var err error if opts != nil { t.StripPath = opts["strip"] t.PrependPath = opts["prepend"] t.TLSSkipVerify = opts["tlsskipverify"] == "true" t.Host = opts["host"] t.ProxyProto = opts["pxyproto"] == "true" // if Host is "dst", we don't need a special transport to override the sni because // this is already the default behavior. if t.Host != "" && t.Host != "dst" && (t.URL.Scheme == "https" || opts["proto"] == "https") { t.Transport = transport.NewTransport(&tls.Config{ServerName: t.Host, InsecureSkipVerify: t.TLSSkipVerify}) } if opts["redirect"] != "" { t.RedirectCode, err = strconv.Atoi(opts["redirect"]) if err != nil { log.Printf("[ERROR] redirect status code should be numeric in 3xx range. Got: %s", opts["redirect"]) } else if t.RedirectCode < 300 || t.RedirectCode > 399 { t.RedirectCode = 0 log.Printf("[ERROR] redirect status code should be in 3xx range. Got: %s", opts["redirect"]) } } if err = t.ProcessAccessRules(); err != nil { log.Printf("[ERROR] failed to process access rules: %s", err.Error()) } t.AuthScheme = opts["auth"] } r.Targets = append(r.Targets, t) r.weighTargets() } func (r *Route) filter(skip func(t *Target) bool) { var clone []*Target for _, t := range r.Targets { if skip(t) { continue } clone = append(clone, t) } r.Targets = clone r.weighTargets() } func (r *Route) setWeight(service string, weight float64, tags []string) int { loop := func(w float64) int { n := 0 for _, t := range r.Targets { if service != "" && t.Service != service { continue } if len(tags) > 0 && !contains(t.Tags, tags) { continue } n++ t.FixedWeight = w } return n } // if we have multiple matching targets // then we need to distribute the total // weight across all of them since the rule // states to assign only that percentage // of traffic to all matching routes combined. n := loop(0) w := weight / float64(n) loop(w) if n > 0 { r.weighTargets() } return n } func contains(src, dst []string) bool { for _, d := range dst { found := false for _, s := range src { if s == d { found = true break } } if !found { return false } } return true } func (r *Route) TargetConfig(t *Target, addWeight bool) string { s := fmt.Sprintf("route add %s %s %s", t.Service, r.Host+r.Path, t.URL) if addWeight { s += fmt.Sprintf(" weight %2.4f", t.Weight) } else if t.FixedWeight > 0 { s += fmt.Sprintf(" weight %.4f", t.FixedWeight) } if len(t.Tags) > 0 { s += fmt.Sprintf(" tags %q", strings.Join(t.Tags, ",")) } if len(t.Opts) > 0 { var keys []string for k := range t.Opts { keys = append(keys, k) } sort.Strings(keys) var vals []string for _, k := range keys { vals = append(vals, k+"="+t.Opts[k]) } s += fmt.Sprintf(" opts \"%s\"", strings.Join(vals, " ")) } return s } // config returns the route configuration in the config language. // with the weights specified by the user. func (r *Route) config(addWeight bool) []string { var cfg []string for _, t := range r.Targets { if t.Weight <= 0 { continue } cfg = append(cfg, r.TargetConfig(t, addWeight)) } return cfg } // maxSlots defines the maximum number of slots on the ring for // weighted round-robin distribution for a single route. Consequently, // this then defines the maximum number of separate instances that can // serve a single route. maxSlots must be a power of ten. const maxSlots = 1e4 // 10000 // weighTargets computes the share of traffic each target receives based // on its weight and the weight of the other targets. // // Traffic is first distributed to targets with a fixed weight. If the sum of // all fixed weights exceeds 100% then they are normalized to 100%. // // Targets with a dynamic weight will receive an equal share of the remaining // traffic if there is any left. func (r *Route) weighTargets() { // how big is the fixed weighted traffic? var nFixed int var sumFixed float64 for _, t := range r.Targets { if t.FixedWeight > 0 { nFixed++ sumFixed += t.FixedWeight } } // if there are no targets with fixed weight then each target simply gets // an equal amount of traffic if nFixed == 0 { w := 1.0 / float64(len(r.Targets)) for _, t := range r.Targets { t.Weight = w } r.wTargets = r.Targets return } // normalize fixed weights up (sumFixed < 1) or down (sumFixed > 1) scale := 1.0 if sumFixed > 1 || (nFixed == len(r.Targets) && sumFixed < 1) { scale = 1 / sumFixed } // compute the weight for the targets with dynamic weights dynamic := (1 - sumFixed) / float64(len(r.Targets)-nFixed) if dynamic < 0 { dynamic = 0 } // assign the actual weight to each target for _, t := range r.Targets { if t.FixedWeight > 0 { t.Weight = t.FixedWeight * scale } else { t.Weight = dynamic } } // distribute the targets on a ring suitable for weighted round-robin // distribution // // This is done in two steps: // // Step one determines the necessary ring size to distribute the targets // according to their weight with reasonable accuracy. For example, two // targets with 50% weight fit in a ring of size 2 whereas two targets with // 10% and 90% weight require a ring of size 10. // // To keep it simple we allocate 10000 slots which provides slots to all // targets with at least a weight of 0.01%. In addition, we guarantee that // every target with a weight > 0 gets at least one slot. The case where // all targets get an equal share of traffic is handled earlier so this is // for situations with some fixed weight. // // Step two distributes the targets onto the ring spacing them out evenly // so that iterating over the ring performs the weighted rr distribution. // For example, a 50/50 distribution on a ring of size 10 should be // 0101010101 instead of 0000011111. // // To ensure that targets with smaller weights are properly placed we place // them on the ring first by sorting the targets by slot count. // // TODO(fs): I assume that this is some sort of mathematical problem // (coloring, optimizing, ...) but I don't know which. Happy to make this // more formal, if possible. // slots := make(byN, len(r.Targets)) usedSlots := 0 for i, t := range r.Targets { n := int(float64(maxSlots) * t.Weight) if n == 0 && t.Weight > 0 { n = 1 } slots[i].i = i slots[i].n = n usedSlots += n } sort.Sort(slots) targets := make([]*Target, usedSlots) for _, s := range slots { if s.n <= 0 { continue } next, step := 0, usedSlots/s.n for range s.n { // find the next empty slot for targets[next] != nil { next = (next + 1) % usedSlots } // use slot and move to next one targets[next] = r.Targets[s.i] next = (next + step) % usedSlots } } r.wTargets = targets } type byN []struct{ i, n int } func (r byN) Len() int { return len(r) } func (r byN) Swap(i, j int) { r[i], r[j] = r[j], r[i] } func (r byN) Less(i, j int) bool { return r[i].n < r[j].n } ================================================ FILE: route/route_bench_test.go ================================================ package route import ( "bytes" "fmt" "net/http" "sync" "testing" ) var ( b5Routes Table b10Routes Table b100Routes Table b500Routes Table once sync.Once ) // initRoutes is used for lazy one time initialization of the test data for // the parallel benchmarks via once func initRoutes() { b5Routes = makeRoutes(1, 5, 1, 6) b10Routes = makeRoutes(1, 5, 2, 6) b100Routes = makeRoutes(10, 5, 2, 24) b500Routes = makeRoutes(10, 10, 5, 24) } func BenchmarkPrefixMatcherRndPicker5Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.RunParallel(func(b *testing.PB) { benchmarkGet(b5Routes, prefixMatcher, rndPicker, b) }) } func BenchmarkPrefixMatcherRRPicker5Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.RunParallel(func(b *testing.PB) { benchmarkGet(b5Routes, prefixMatcher, rrPicker, b) }) } func BenchmarkPrefixMatcherRndPicker10Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b10Routes, prefixMatcher, rndPicker, b) }) } func BenchmarkPrefixMatcherRRPicker10Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b10Routes, prefixMatcher, rrPicker, b) }) } func BenchmarkPrefixMatcherRndPicker100Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b100Routes, prefixMatcher, rndPicker, b) }) } func BenchmarkPrefixMatcherRRPicker100Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b100Routes, prefixMatcher, rrPicker, b) }) } func BenchmarkPrefixMatcherRndPicker500Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b500Routes, prefixMatcher, rndPicker, b) }) } func BenchmarkPrefixMatcherRRPicker500Routes(b *testing.B) { once.Do(initRoutes) b.ResetTimer() b.SetParallelism(3) b.RunParallel(func(b *testing.PB) { benchmarkGet(b500Routes, prefixMatcher, rrPicker, b) }) } // makeRoutes builds a set of routes for a set of domains // and target urls. For each domain all paths up to depth // are constructed and all host/path combinations have the // same target URLs. The number of generated routes is // domains * paths * depth. func makeRoutes(domains, paths, depth, urls int) Table { s := "" for i := range domains { prefix := fmt.Sprintf("www.host-%d.com/", i) for range paths { for k := range depth { prefix += fmt.Sprintf("path-%d/", k) for range urls { s += fmt.Sprintf("route add svc %s http://host:12345/\n", prefix) } } } } t, err := NewTable(bytes.NewBufferString(s)) if err != nil { panic(err) } return t } // makeRequests builds a list of http.Request objects with an // additional path for benchmarking. func makeRequests(t Table) []*http.Request { reqs := []*http.Request{} for host, hr := range t { for _, r := range hr { req := &http.Request{Host: host, RequestURI: r.Path + "/some/additional/path"} reqs = append(reqs, req) } } return reqs } // benchmarkGet runs the benchmark on the Table.Lookup() function with the // given matcher and picker functions. func benchmarkGet(t Table, match matcher, pick picker, pb *testing.PB) { reqs := makeRequests(t) k, n := len(reqs), 0 //Glob Matching True for pb.Next() { t.Lookup(reqs[n%k], pick, match, globCache, globEnabled) n++ } } ================================================ FILE: route/route_def.go ================================================ package route type Cmd string const ( RouteAddCmd Cmd = "route add" RouteDelCmd Cmd = "route del" RouteWeightCmd Cmd = "route weight" ) type RouteDef struct { Opts map[string]string `json:"opts,omitempty"` Cmd Cmd `json:"cmd"` Service string `json:"service"` Src string `json:"src"` Dst string `json:"dst"` Tags []string `json:"tags,omitempty"` Weight float64 `json:"weight"` } ================================================ FILE: route/routes.go ================================================ package route // Routes stores a list of routes usually for a single host. type Routes []*Route // find returns the route with the given path and returns nil if none was found. func (rt Routes) find(path string) *Route { for _, r := range rt { if r.Path == path { return r } } return nil } // sort by path in reverse order (most to least specific) func (rt Routes) Len() int { return len(rt) } func (rt Routes) Swap(i, j int) { rt[i], rt[j] = rt[j], rt[i] } func (rt Routes) Less(i, j int) bool { return rt[j].Path < rt[i].Path } ================================================ FILE: route/table.go ================================================ package route import ( "bytes" "errors" "fmt" "log" "net" "net/http" "net/url" "sort" "strings" "sync/atomic" gkm "github.com/go-kit/kit/metrics" "github.com/gobwas/glob" "github.com/fabiolb/fabio/metrics" ) var errInvalidPrefix = errors.New("route: prefix must not be empty") var errInvalidTarget = errors.New("route: target must not be empty") var errNoMatch = errors.New("route: no target match") // table stores the active routing table. Must never be nil. var table atomic.Value // package global metrics bits // init initializes the routing table. func init() { table.Store(make(Table)) np := metrics.DiscardProvider{} SetMetricsProvider(np) } type metrix struct { histogram gkm.Histogram rxCounter gkm.Counter txCounter gkm.Counter } var counters metrix // makeMetricKey creates a unique key for a target's metric labels. func makeMetricKey(service, host, path, target string) string { return service + "\x00" + host + "\x00" + path + "\x00" + target } // parseMetricKey extracts label values from a metric key. func parseMetricKey(key string) (service, host, path, target string) { parts := strings.Split(key, "\x00") if len(parts) == 4 { return parts[0], parts[1], parts[2], parts[3] } return "", "", "", "" } // collectTableMetricKeys extracts all metric label keys from a routing table. func collectTableMetricKeys(t Table) map[string]struct{} { keys := make(map[string]struct{}) for _, routes := range t { for _, r := range routes { for _, target := range r.Targets { key := makeMetricKey(target.Service, r.Host, r.Path, target.URL.String()) keys[key] = struct{}{} } } } return keys } // cleanupStaleMetrics removes metric label combinations that are no longer active. // It compares the old table against the new table to find removed targets. func cleanupStaleMetrics(oldTable, newTable Table) { oldKeys := collectTableMetricKeys(oldTable) newKeys := collectTableMetricKeys(newTable) log.Printf("[INFO] cleanupStaleMetrics: oldKeys=%d newKeys=%d", len(oldKeys), len(newKeys)) // Find and delete stale labels (in old but not in new) for key := range oldKeys { if _, exists := newKeys[key]; !exists { service, host, path, target := parseMetricKey(key) log.Printf("[INFO] Cleaning up stale metrics for service=%s host=%s path=%s target=%s", service, host, path, target) // Delete from each metric type if they support deletion if dh, ok := counters.histogram.(metrics.DeletableHistogram); ok { deleted := dh.DeleteLabelValues(service, host, path, target) log.Printf("[INFO] Histogram delete returned: %v", deleted) } else { log.Printf("[WARN] Histogram does not implement DeletableHistogram, type=%T", counters.histogram) } if dc, ok := counters.rxCounter.(metrics.DeletableCounter); ok { dc.DeleteLabelValues(service, host, path, target) } if dc, ok := counters.txCounter.(metrics.DeletableCounter); ok { dc.DeleteLabelValues(service, host, path, target) } } } } func SetMetricsProvider(p metrics.Provider) { counters.histogram = p.NewHistogram("route", "service", "host", "path", "target") counters.rxCounter = p.NewCounter("route.rx", "service", "host", "path", "target") counters.txCounter = p.NewCounter("route.tx", "service", "host", "path", "target") } // GetTable returns the active routing table. The function // is safe to be called from multiple goroutines and the // value is never nil. func GetTable() Table { return table.Load().(Table) } // SetTable sets the active routing table. A nil value // logs a warning and is ignored. The function is safe // to be called from multiple goroutines. // It also cleans up stale Prometheus metrics for targets that are no longer active. func SetTable(t Table) { if t == nil { log.Print("[WARN] Ignoring nil routing table") return } // Get the old table to compare against oldTable := GetTable() // Store the new table FIRST, then cleanup stale metrics. // This order is important to avoid a race condition where traffic // could recreate the metrics between delete and table swap. table.Store(t) // Clean up metrics for removed targets after storing the new table cleanupStaleMetrics(oldTable, t) } // Table contains a set of routes grouped by host. // The host routes are sorted from most to least specific // by sorting the routes in reverse order by path. type Table map[string]Routes // hostpath splits a 'host/path' prefix into 'host' and '/path' or it returns a // ':port' prefix as ':port' and ” since there is no path component for TCP // connections. func hostpath(prefix string) (host string, path string) { if strings.HasPrefix(prefix, ":") { return prefix, "" } p := strings.SplitN(prefix, "/", 2) if len(p) == 1 { return p[0], "/" } return p[0], "/" + p[1] } func NewTable(b *bytes.Buffer) (t Table, err error) { defs, err := Parse(b) if err != nil { return nil, err } t = make(Table) for _, d := range defs { switch d.Cmd { case RouteAddCmd: err = t.addRoute(d) case RouteDelCmd: err = t.delRoute(d) case RouteWeightCmd: err = t.weighRoute(d) default: err = fmt.Errorf("route: invalid command: %s", d.Cmd) } if err != nil { return nil, err } } // Sort the route table for each hostname for _, h := range t { sort.Sort(h) } return t, nil } func NewTableCustom(defs *[]RouteDef) (t Table, err error) { t = make(Table) for _, d := range *defs { switch d.Cmd { case RouteAddCmd: err = t.addRoute(&d) case RouteDelCmd: err = t.delRoute(&d) case RouteWeightCmd: err = t.weighRoute(&d) default: err = fmt.Errorf("route: invalid command: %s", d.Cmd) } if err != nil { return nil, err } } // Sort the route table for each hostname for _, h := range t { sort.Sort(h) } return t, nil } // addRoute adds a new route prefix -> target for the given service. func (t Table) addRoute(d *RouteDef) error { host, path := hostpath(d.Src) host = strings.ToLower(host) // maintain compatibility with parseURLPrefixTag if d.Src == "" { return errInvalidPrefix } if d.Dst == "" { return errInvalidTarget } targetURL, err := url.Parse(d.Dst) if err != nil { return fmt.Errorf("route: invalid target. %s", err) } switch { // add new host case t[host] == nil: g, err := glob.Compile(path) if err != nil { return err } r := &Route{Host: host, Path: path, Glob: g} r.addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts) t[host] = Routes{r} // add new route to existing host case t[host].find(path) == nil: g, err := glob.Compile(path) if err != nil { return err } r := &Route{Host: host, Path: path, Glob: g} r.addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts) t[host] = append(t[host], r) // add new target to existing route default: t[host].find(path).addTarget(d.Service, targetURL, d.Weight, d.Tags, d.Opts) } return nil } func (t Table) weighRoute(d *RouteDef) error { host, path := hostpath(d.Src) if d.Src == "" { return errInvalidPrefix } if t[host] == nil || t[host].find(path) == nil { return errNoMatch } if n := t[host].find(path).setWeight(d.Service, d.Weight, d.Tags); n == 0 { return errNoMatch } return nil } // delRoute removes one or more routes depending on the arguments. // If service, prefix and target are provided then only this route // is removed. Are only service and prefix provided then all routes // for this service and prefix are removed. This removes all active // instances of the service from the route. If only the service is // provided then all routes for this service are removed. The service // will no longer receive traffic. Routes with no targets are removed. func (t Table) delRoute(d *RouteDef) error { switch { case len(d.Tags) > 0: for _, routes := range t { for _, r := range routes { r.filter(func(tg *Target) bool { return (d.Service == "" || tg.Service == d.Service) && contains(tg.Tags, d.Tags) }) } } case d.Src == "" && d.Dst == "": for _, routes := range t { for _, r := range routes { r.filter(func(tg *Target) bool { return tg.Service == d.Service }) } } case d.Dst == "": r := t.route(hostpath(d.Src)) if r == nil { return nil } r.filter(func(tg *Target) bool { return tg.Service == d.Service }) default: targetURL, err := url.Parse(d.Dst) if err != nil { return fmt.Errorf("route: invalid target. %s", err) } r := t.route(hostpath(d.Src)) if r == nil { return nil } r.filter(func(tg *Target) bool { return tg.Service == d.Service && tg.URL.String() == targetURL.String() }) } // remove all routes without targets for host, routes := range t { var clone Routes for _, r := range routes { if len(r.Targets) == 0 { continue } clone = append(clone, r) } t[host] = clone } // remove all hosts without routes for host, routes := range t { if len(routes) == 0 { delete(t, host) } } return nil } // route finds the route for host/path or returns nil if none exists. func (t Table) route(host, path string) *Route { routes := t[host] if routes == nil { return nil } return routes.find(path) } // normalizeHost returns the hostname from the request // and removes the default port if present. func normalizeHost(host string, tls bool) string { return strings.ToLower(normalizeHostNoLower(host, tls)) } func normalizeHostNoLower(host string, tls bool) string { if !tls && strings.HasSuffix(host, ":80") { return host[:len(host)-len(":80")] } if tls && strings.HasSuffix(host, ":443") { return host[:len(host)-len(":443")] } return host } // matchingHosts returns all keys (host name patterns) from the // routing table which match the normalized request hostname. func (t Table) matchingHosts(req *http.Request, globCache *GlobCache) (hosts []string) { host := normalizeHost(req.Host, req.TLS != nil) for pattern := range t { normpat := normalizeHost(pattern, req.TLS != nil) // Issue 548 // //Get Compiled Glob from LRU cache g, err := globCache.Get(normpat) if err != nil { log.Print("[Error] Compiling glob - ", err) g = glob.MustCompile(normpat) } if g.Match(host) { hosts = append(hosts, pattern) } } hosts = sortHostsReverseHostPort(hosts) return } // Issue 548 - Added separate func // // matchingHostNoGlob returns the route from the // routing table which matches the normalized request hostname. func (t Table) matchingHostNoGlob(req *http.Request) (hosts []string) { host := normalizeHostNoLower(req.Host, req.TLS != nil) for pattern := range t { normpat := normalizeHost(pattern, req.TLS != nil) if normpat == host { hosts = append(hosts, strings.ToLower(pattern)) } } hosts = sortHostsReverseHostPort(hosts) return } func sortHostsReverseHostPort(hosts []string) []string { // Issue 506: multiple glob patterns hosts in wrong order // // DNS names have their most specific part at the front. In order to sort // them from most specific to least specific a lexicographic sort will // return the wrong result since it sorts by host name. *.foo.com will come // before *.a.foo.com even though the latter is more specific. To achieve // the correct result we need to reverse the strings, sort them and then // reverse them again. if len(hosts) < 2 { return hosts } for i, h := range hosts { hosts[i] = ReverseHostPort(h) } sort.Sort(sort.Reverse(sort.StringSlice(hosts))) for i, h := range hosts { hosts[i] = ReverseHostPort(h) } return hosts } // ReverseHostPort returns its argument string reversed rune-wise left to // right. If s includes a port, only the host part is reversed. func ReverseHostPort(s string) string { h, p, _ := net.SplitHostPort(s) if h == "" { h = s } // Taken from https://github.com/golang/example/blob/master/stringutil/reverse.go r := []rune(h) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } if p == "" { return string(r) } else { return net.JoinHostPort(string(r), p) } } // Lookup finds a target url based on the current matcher and picker // or nil if there is none. It first checks the routes for the host // and if none matches then it falls back to generic routes without // a host. This is useful for a catch-all '/' rule. func (t Table) Lookup(req *http.Request, pick picker, match matcher, globCache *GlobCache, globDisabled bool) (target *Target) { var hosts []string // find matching hosts for the request // and add "no host" as the fallback option // if globDisabled then match without Glob // Issue 548 if globDisabled { hosts = t.matchingHostNoGlob(req) } else { hosts = t.matchingHosts(req, globCache) } hosts = append(hosts, "") for _, h := range hosts { if target = t.lookup(h, req.URL.Path, pick, match); target != nil { if target.RedirectCode != 0 { req.URL.Host = req.Host target.BuildRedirectURL(req.URL) // build redirect url and cache in target if target.RedirectURL.Scheme == req.Header.Get("X-Forwarded-Proto") && target.RedirectURL.Host == req.Host && target.RedirectURL.Path == req.URL.Path { log.Print("[INFO] Skipping redirect with same scheme, host and path") continue } } break } } return target } func (t Table) LookupHost(host string, pick picker) *Target { return t.lookup(host, "/", pick, prefixMatcher) } func (t Table) lookup(host, path string, pick picker, match matcher) *Target { host = strings.ToLower(host) // routes are always added lowercase for _, r := range t[host] { if match(path, r) { n := len(r.Targets) if n == 0 { return nil } var target *Target if n == 1 { target = r.Targets[0] } else { target = pick(r) } return target } } return nil } func (t Table) config(addWeight bool) []string { var hosts []string for host := range t { if host != "" { hosts = append(hosts, host) } } sort.Sort(sort.Reverse(sort.StringSlice(hosts))) // entries without host come last hosts = append(hosts, "") var cfg []string for _, host := range hosts { for _, routes := range t[host] { cfg = append(cfg, routes.config(addWeight)...) } } return cfg } // String returns the routing table as config file which can // be read by Parse() again. func (t Table) String() string { return strings.Join(t.config(false), "\n") } // Dump returns the routing table as a detailed func (t Table) Dump() string { w := new(bytes.Buffer) hosts := []string{} for k := range t { hosts = append(hosts, k) } sort.Strings(hosts) last := func(n, total int) bool { return n == total-1 } for i, h := range hosts { fmt.Fprintf(w, "+-- host=%s\n", h) routes := t[h] for j, r := range routes { p0 := "| " if last(i, len(hosts)) { p0 = " " } p1 := "|-- " if last(j, len(routes)) { p1 = "+-- " } fmt.Fprintf(w, "%s%spath=%s\n", p0, p1, r.Path) m := map[*Target]int{} for _, t := range r.wTargets { m[t] += 1 } total := len(r.wTargets) k := 0 for t, n := range m { p1 := "| " if last(j, len(routes)) { p1 = " " } p2 := "|-- " if last(k, len(m)) { p2 = "+-- " } weight := float64(n) / float64(total) fmt.Fprintf(w, "%s%s%saddr=%s weight %2.2f slots %d/%d\n", p0, p1, p2, t.URL.Host, weight, n, total) k++ } } } return w.String() } ================================================ FILE: route/table_registry_test.go ================================================ package route // func TestSyncRegistry(t *testing.T) { // oldRegistry := ServiceRegistry // ServiceRegistry = newStubRegistry() // defer func() { ServiceRegistry = oldRegistry }() // // tbl := make(Table) // tbl.addRoute(&RouteDef{Service: "svc-a", Src: "/aaa", Dst: "http://localhost:1234", Weight: 1}) // tbl.addRoute(&RouteDef{Service: "svc-b", Src: "/bbb", Dst: "http://localhost:5678", Weight: 1}) // if got, want := ServiceRegistry.Names(), []string{"svc-a._./aaa.localhost_1234", "svc-b._./bbb.localhost_5678"}; !reflect.DeepEqual(got, want) { // t.Fatalf("got %v want %v", got, want) // } // // tbl.delRoute(&RouteDef{Service: "svc-b", Src: "/bbb", Dst: "http://localhost:5678"}) // syncRegistry(tbl) // if got, want := ServiceRegistry.Names(), []string{"svc-a._./aaa.localhost_1234"}; !reflect.DeepEqual(got, want) { // t.Fatalf("got %v want %v", got, want) // } // } // // func newStubRegistry() metrics.Registry { // return &stubRegistry{names: make(map[string]bool)} // } // // type stubRegistry struct { // names map[string]bool // } // // func (p *stubRegistry) Names() []string { // n := []string{} // for k := range p.names { // n = append(n, k) // } // sort.Strings(n) // return n // } // // func (p *stubRegistry) Unregister(name string) { // delete(p.names, name) // } // // func (p *stubRegistry) UnregisterAll() { // p.names = map[string]bool{} // } // // func (p *stubRegistry) GetCounter(name string) metrics.Counter { // p.names[name] = true // return metrics.NoopCounter{} // } // // func (p *stubRegistry) GetTimer(name string) metrics.Timer { // p.names[name] = true // return metrics.NoopTimer{} // } ================================================ FILE: route/table_test.go ================================================ package route import ( "bytes" "crypto/tls" "fmt" "math" "net/http" "net/http/httptest" "reflect" "strconv" "strings" "testing" ) const ( // helper constants for the Lookup function globEnabled = false globDisabled = true ) // Global GlobCache for Testing var globCache = NewGlobCache(1000) func TestTableParse(t *testing.T) { genRoutes := func(n int, format string) (a []string) { for i := range n { a = append(a, fmt.Sprintf(format, i)) } return a } tests := []struct { desc string in, out []string }{ {"1 service, 1 prefix", []string{ `route add svc-a / http://aaa.com/`, }, []string{ `route add svc-a / http://aaa.com/ weight 1.0000`, }, }, {"1 service, 1 prefix, 3 instances", []string{ `route add svc-a / http://aaa.com:1111/`, `route add svc-a / http://aaa.com:2222/`, `route add svc-a / http://aaa.com:3333/`, }, []string{ `route add svc-a / http://aaa.com:1111/ weight 0.3333`, `route add svc-a / http://aaa.com:2222/ weight 0.3333`, `route add svc-a / http://aaa.com:3333/ weight 0.3333`, }, }, {"1 service, 1 prefix with option", []string{ `route add svc-a / http://aaa.com/ opts "strip=/foo"`, `route add svc-b / http://bbb.com/ opts "strip=/bar"`, }, []string{ `route add svc-a / http://aaa.com/ weight 0.5000 opts "strip=/foo"`, `route add svc-b / http://bbb.com/ weight 0.5000 opts "strip=/bar"`, }, }, {"1 service, 1 prefix, 2 instances with different options", []string{ `route add svc-a / http://aaa.com/ opts "strip=/foo"`, `route add svc-b / http://bbb.com/ opts "strip=/bar"`, }, []string{ `route add svc-a / http://aaa.com/ weight 0.5000 opts "strip=/foo"`, `route add svc-b / http://bbb.com/ weight 0.5000 opts "strip=/bar"`, }, }, {"2 service, 1 prefix", []string{ `route add svc-a / http://aaa.com/`, `route add svc-b / http://bbb.com/`, }, []string{ `route add svc-a / http://aaa.com/ weight 0.5000`, `route add svc-b / http://bbb.com/ weight 0.5000`, }, }, {"1 service, 2 prefix", []string{ `route add svc-a /one http://aaa.com/`, `route add svc-a /two http://aaa.com/`, }, []string{ `route add svc-a /two http://aaa.com/ weight 1.0000`, `route add svc-a /one http://aaa.com/ weight 1.0000`, }, }, {"2 service, 2 prefix", []string{ `route add svc-a /a http://aaa.com/`, `route add svc-b /b http://bbb.com/`, }, []string{ `route add svc-b /b http://bbb.com/ weight 1.0000`, `route add svc-a /a http://aaa.com/ weight 1.0000`, }, }, {"sort by more specific prefix", []string{ `route add svc-a / http://aaa.com/`, `route add svc-b /b http://bbb.com/`, }, []string{ `route add svc-b /b http://bbb.com/ weight 1.0000`, `route add svc-a / http://aaa.com/ weight 1.0000`, }, }, {"sort prefix with host before prefix without host", []string{ `route add svc-a / http://aaa.com/`, `route add svc-b b.com/ http://bbb.com/`, }, []string{ `route add svc-b b.com/ http://bbb.com/ weight 1.0000`, `route add svc-a / http://aaa.com/ weight 1.0000`, }, }, {"add more specific prefix to existing host", []string{ `route add svc-a a.com/ http://aaa.com/`, `route add svc-a a.com/a http://aaa.com/`, }, []string{ `route add svc-a a.com/a http://aaa.com/ weight 1.0000`, `route add svc-a a.com/ http://aaa.com/ weight 1.0000`, }, }, {"delete route by service, path and target", []string{ `route add svc-a / http://aaa.com/`, `route add svc-b / http://bbb.com/`, `route del svc-b / http://bbb.com/`, }, []string{ `route add svc-a / http://aaa.com/ weight 1.0000`, }, }, {"delete route by service and path", []string{ `route add svc-a / http://aaa.com/`, `route add svc-a / http://aaa.com:2222/`, `route add svc-b / http://bbb.com/`, `route del svc-a /`, }, []string{ `route add svc-b / http://bbb.com/ weight 1.0000`, }, }, {"delete route by service", []string{ `route add svc-a /a http://aaa.com/`, `route add svc-a / http://aaa.com/`, `route add svc-b / http://bbb.com/`, `route del svc-a`, }, []string{ `route add svc-b / http://bbb.com/ weight 1.0000`, }, }, {"delete route by service and tags", []string{ `route add svc-a /a http://aaa.com/ tags "a,b"`, `route add svc-a / http://aaa.com/ tags "b,c"`, `route add svc-b / http://bbb.com/ tags "c,d"`, `route del svc-a tags "a,b"`, }, []string{ `route add svc-a / http://aaa.com/ weight 0.5000 tags "b,c"`, `route add svc-b / http://bbb.com/ weight 0.5000 tags "c,d"`, }, }, {"delete route by tags", []string{ `route add svc-a /a http://aaa.com/ tags "a,b"`, `route add svc-a / http://aaa.com/ tags "b,c"`, `route add svc-b / http://bbb.com/ tags "c,d"`, `route del tags "b"`, }, []string{ `route add svc-b / http://bbb.com/ weight 1.0000 tags "c,d"`, }, }, {"weigh fixed weight 0 -> auto distribution", []string{ `route add svc / http://bar:111/ weight 0`, }, []string{ `route add svc / http://bar:111/ weight 1.0000`, }, }, {"weigh only fixed weights and sum(fixedWeight) < 1 -> normalize to 100%", []string{ `route add svc / http://bar:111/ weight 0.2`, `route add svc / http://bar:222/ weight 0.3`, }, []string{ `route add svc / http://bar:111/ weight 0.4000`, `route add svc / http://bar:222/ weight 0.6000`, }, }, {"weigh only fixed weights and sum(fixedWeight) > 1 -> normalize to 100%", []string{ `route add svc / http://bar:111/ weight 2`, `route add svc / http://bar:222/ weight 3`, }, []string{ `route add svc / http://bar:111/ weight 0.4000`, `route add svc / http://bar:222/ weight 0.6000`, }, }, {"weigh multiple entries for same instance with no fixed weight -> de-duplication", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:111/`, }, []string{ `route add svc / http://bar:111/ weight 1.0000`, }, }, {"weigh multiple entries with no fixed weight -> even distribution", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:222/`, }, []string{ `route add svc / http://bar:111/ weight 0.5000`, `route add svc / http://bar:222/ weight 0.5000`, }, }, {"weigh multiple entries with de-dup and no fixed weight -> even distribution", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:111/`, `route add svc / http://bar:222/`, }, []string{ `route add svc / http://bar:111/ weight 0.5000`, `route add svc / http://bar:222/ weight 0.5000`, }, }, {"weigh mixed fixed and auto weights -> even distribution of remaining weight across non-fixed weighted targets", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:222/`, `route add svc / http://bar:333/ weight 0.5`, }, []string{ `route add svc / http://bar:111/ weight 0.2500`, `route add svc / http://bar:222/ weight 0.2500`, `route add svc / http://bar:333/ weight 0.5000`, }, }, {"weigh fixed weight == 100% -> route only to fixed weighted targets", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:222/ weight 0.2500`, `route add svc / http://bar:333/ weight 0.7500`, }, []string{ `route add svc / http://bar:222/ weight 0.2500`, `route add svc / http://bar:333/ weight 0.7500`, }, }, {"weigh fixed weight > 100% -> route only to fixed weighted targets and normalize weight", []string{ `route add svc / http://bar:111/`, `route add svc / http://bar:222/ weight 1`, `route add svc / http://bar:333/ weight 3`, }, []string{ `route add svc / http://bar:222/ weight 0.2500`, `route add svc / http://bar:333/ weight 0.7500`, }, }, {"weigh dynamic weight matched on service name", []string{ `route add svca / http://bar:111/`, `route add svcb / http://bar:222/`, `route add svcb / http://bar:333/`, `route weight svcb / weight 0.1`, }, []string{ `route add svca / http://bar:111/ weight 0.9000`, `route add svcb / http://bar:222/ weight 0.0500`, `route add svcb / http://bar:333/ weight 0.0500`, }, }, {"weigh dynamic weight matched on service name and tags", []string{ `route add svc / http://bar:111/ tags "a"`, `route add svc / http://bar:222/ tags "b"`, `route add svc / http://bar:333/ tags "b"`, `route weight svc / weight 0.1 tags "b"`, }, []string{ `route add svc / http://bar:111/ weight 0.9000 tags "a"`, `route add svc / http://bar:222/ weight 0.0500 tags "b"`, `route add svc / http://bar:333/ weight 0.0500 tags "b"`, }, }, {"weigh dynamic weight matched on tags", []string{ `route add svca / http://bar:111/ tags "a"`, `route add svcb / http://bar:222/ tags "b"`, `route add svcb / http://bar:333/ tags "b"`, `route weight / weight 0.1 tags "b"`, }, []string{ `route add svca / http://bar:111/ weight 0.9000 tags "a"`, `route add svcb / http://bar:222/ weight 0.0500 tags "b"`, `route add svcb / http://bar:333/ weight 0.0500 tags "b"`, }, }, {"weigh more than 1000 routes", genRoutes(1234, `route add svc / http://bar:%d/`), genRoutes(1234, `route add svc / http://bar:%d/ weight 0.0008`), }, {"weigh more than 1000 routes with a fixed route target", func() (a []string) { a = genRoutes(1234, `route add svc / http://bar:%d/`) a = append(a, `route add svc / http://static:12345/ tags "a"`) a = append(a, `route weight svc / weight 0.2 tags "a"`) return a }(), func() (a []string) { a = genRoutes(1234, `route add svc / http://bar:%d/ weight 0.0006`) a = append(a, `route add svc / http://static:12345/ weight 0.2000 tags "a"`) return a }(), }, } atof := func(s string) float64 { n, err := strconv.ParseFloat(s, 64) if err != nil { panic(err) } return n } for _, tt := range tests { // perform a test which parses the tt.in routes into a table and // compares the weighted, generated routing table with tt.out. verify, // that the distribution of the target URLs for each prefix in the // generated routing table matches the weight This test assumes that // the table generates the correct routing table but does not test the // actual lookup which it probably should. t.Run(tt.desc, func(t *testing.T) { // parse the routes tbl, err := NewTable(bytes.NewBufferString(strings.Join(tt.in, "\n"))) if err != nil { t.Fatalf("got %v want nil", err) } // compare the generated routes with the normalized weights if got, want := tbl.config(true), tt.out; !reflect.DeepEqual(got, want) { t.Errorf("got\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) } // check that the weights returned in the generated config match // the distribution in the wTargets array of the corresponding route. checked := map[string]bool{} for _, s := range tt.out { // route add ... path := strings.Fields(s)[3] // if we have already checked this path then skip this. // Otherwise, this test becomes O(n^2) and will time out for // the large number of routes. if checked[path] { continue } checked[path] = true // fetch the route r := tbl.route(hostpath(path)) if r == nil { t.Fatalf("got nil want route %s", path) } // check that there are at least some slots if len(r.wTargets) == 0 { t.Fatalf("got 0 targets want some") } // pre-generate the target urls for comparison as this // will otherwise slow the test down significantly targetURLs := make([]string, len(r.wTargets)) for i, tg := range r.wTargets { targetURLs[i] = tg.URL.Scheme + "://" + tg.URL.Host + tg.URL.Path } // count how often the 'url' from 'route add svc ' // appears in the list of wTargets for all the URLs // from the routes to determine whether the actual // distribution of each target within the wTarget slice // matches what we expect for _, s := range tt.out { // route add weight ...`, p := strings.Fields(s) // skip if the path doesn't match if path != p[3] { continue } // count how often the target url appears in the list of wTargets count := 0 for _, u := range targetURLs { if u == p[4] { count++ } } // calc the weight as nSlots/totalSlots gotWeight := float64(count) / float64(len(r.wTargets)) // round the weight down to the number of decimal points // supported by maxSlots gotWeight = float64(int(gotWeight*float64(maxSlots))) / float64(maxSlots) // compare to the weight from the generated config wantWeight := atof(p[6]) // check that the actual weight is within 2% of the computed weight if math.Abs(gotWeight-wantWeight) > 0.02 { t.Errorf("got weight %f want %f", gotWeight, wantWeight) } // TODO(fs): verify distriibution of targets across the ring // TODO(fs): verify lookup with 'rr' works as expected. Current test is by proxy of generated config. } } }) } } func TestNormalizeHost(t *testing.T) { tests := []struct { req *http.Request host string }{ {&http.Request{Host: "foo.com"}, "foo.com"}, {&http.Request{Host: "foo.com:80"}, "foo.com"}, {&http.Request{Host: "foo.com:81"}, "foo.com:81"}, {&http.Request{Host: "foo.com", TLS: &tls.ConnectionState{}}, "foo.com"}, {&http.Request{Host: "foo.com:443", TLS: &tls.ConnectionState{}}, "foo.com"}, {&http.Request{Host: "foo.com:444", TLS: &tls.ConnectionState{}}, "foo.com:444"}, } for i, tt := range tests { if got, want := normalizeHost(tt.req.Host, tt.req.TLS != nil), tt.host; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } } // see https://github.com/fabiolb/fabio/issues/448 // for more information on the issue and purpose of this test func TestTableLookupIssue448(t *testing.T) { s := ` route add mock foo.com:80/ https://foo.com/ opts "redirect=301" route add mock aaa.com:80/ http://bbb.com/ opts "redirect=301" route add mock ccc.com:443/bar https://ccc.com/baz opts "redirect=301" route add mock / http://foo.com/ ` tbl, err := NewTable(bytes.NewBufferString(s)) if err != nil { t.Fatal(err) } var tests = []struct { req *http.Request dst string globEnabled bool }{ { req: &http.Request{ Host: "foo.com", URL: mustParse("/"), }, dst: "https://foo.com/", // empty upstream header should follow redirect - standard behavior }, { req: &http.Request{ Host: "foo.com", URL: mustParse("/"), Header: http.Header{"X-Forwarded-Proto": {"http"}}, }, dst: "https://foo.com/", // upstream http request to same host and path should follow redirect }, { req: &http.Request{ Host: "foo.com", URL: mustParse("/"), Header: http.Header{"X-Forwarded-Proto": {"https"}}, TLS: &tls.ConnectionState{}, }, dst: "http://foo.com/", // upstream https request to same host and path should NOT follow redirect" }, { req: &http.Request{ Host: "aaa.com", URL: mustParse("/"), Header: http.Header{"X-Forwarded-Proto": {"http"}}, }, dst: "http://bbb.com/", // upstream http request to different http host should follow redirect }, { req: &http.Request{ Host: "ccc.com", URL: mustParse("/bar"), Header: http.Header{"X-Forwarded-Proto": {"https"}}, TLS: &tls.ConnectionState{}, }, dst: "https://ccc.com/baz", // upstream https request to same https host with different path should follow redirect" }, } for i, tt := range tests { if got, want := tbl.Lookup(tt.req, rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } } func TestTableLookup(t *testing.T) { s := ` route add svc / http://foo.com:800 route add svc /foo http://foo.com:900 route add svc abc.com/ http://foo.com:1000 route add svc abc.com/foo http://foo.com:1500 route add svc abc.com/foo/ http://foo.com:2000 route add svc abc.com/foo/bar http://foo.com:2500 route add svc abc.com/foo/bar/ http://foo.com:3000 route add svc z.abc.com/foo/ http://foo.com:3100 route add svc *.abc.com/ http://foo.com:4000 route add svc *.abc.com/foo/ http://foo.com:5000 route add svc *.aaa.abc.com/ http://foo.com:6000 route add svc *.bbb.abc.com/ http://foo.com:6100 route add svc xyz.com:80/ https://xyz.com ` tbl, err := NewTable(bytes.NewBufferString(s)) if err != nil { t.Fatal(err) } var tests = []struct { req *http.Request dst string globEnabled bool }{ // match on host and path with and without trailing slash {&http.Request{Host: "abc.com", URL: mustParse("/")}, "http://foo.com:1000", globEnabled}, {&http.Request{Host: "abc.com", URL: mustParse("/bar")}, "http://foo.com:1000", globEnabled}, {&http.Request{Host: "abc.com", URL: mustParse("/foo")}, "http://foo.com:1500", globEnabled}, {&http.Request{Host: "abc.com", URL: mustParse("/foo/")}, "http://foo.com:2000", globEnabled}, {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar")}, "http://foo.com:2500", globEnabled}, {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar/")}, "http://foo.com:3000", globEnabled}, // do not match on host but maybe on path {&http.Request{Host: "def.com", URL: mustParse("/")}, "http://foo.com:800", globEnabled}, {&http.Request{Host: "def.com", URL: mustParse("/bar")}, "http://foo.com:800", globEnabled}, {&http.Request{Host: "def.com", URL: mustParse("/foo")}, "http://foo.com:900", globEnabled}, // strip default port {&http.Request{Host: "abc.com:80", URL: mustParse("/")}, "http://foo.com:1000", globEnabled}, {&http.Request{Host: "abc.com:443", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:1000", globEnabled}, // not using default port {&http.Request{Host: "abc.com:443", URL: mustParse("/")}, "http://foo.com:800", globEnabled}, {&http.Request{Host: "abc.com:80", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:800", globEnabled}, // glob match the host {&http.Request{Host: "x.abc.com", URL: mustParse("/")}, "http://foo.com:4000", globEnabled}, {&http.Request{Host: "y.abc.com", URL: mustParse("/abc")}, "http://foo.com:4000", globEnabled}, {&http.Request{Host: "x.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, {&http.Request{Host: "y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, {&http.Request{Host: ".abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, {&http.Request{Host: "x.y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, {&http.Request{Host: "y.abc.com:80", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/")}, "http://foo.com:6000", globEnabled}, {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/foo")}, "http://foo.com:6000", globEnabled}, {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/")}, "http://foo.com:6100", globEnabled}, {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/foo")}, "http://foo.com:6100", globEnabled}, {&http.Request{Host: "y.abc.com:443", URL: mustParse("/foo/"), TLS: &tls.ConnectionState{}}, "http://foo.com:5000", globEnabled}, // exact match has precedence over glob match {&http.Request{Host: "z.abc.com", URL: mustParse("/foo/")}, "http://foo.com:3100", globEnabled}, // explicit port on route {&http.Request{Host: "xyz.com", URL: mustParse("/")}, "https://xyz.com", globEnabled}, } for i, tt := range tests { if got, want := tbl.Lookup(tt.req, rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } } func TestTableLookup_656(t *testing.T) { // A typical HTTPS redirect s := ` route add my-service example.com:80/ https://example.com$path opts "redirect=301" route add my-service example.com/ http://127.0.0.1:3000/ ` tbl, err := NewTable(bytes.NewBufferString(s)) if err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "http://example.com/foo", nil) target := tbl.Lookup(req, rrPicker, prefixMatcher, globCache, globDisabled) if target == nil { t.Fatal("No route match") } if got, want := target.RedirectCode, 301; got != want { t.Errorf("target.RedirectCode = %d, want %d", got, want) } if got, want := fmt.Sprint(target.RedirectURL), "https://example.com/foo"; got != want { t.Errorf("target.RedirectURL = %s, want %s", got, want) } } func TestNewTableCustom(t *testing.T) { var routes []RouteDef var tags = []string{"tag1", "tag2"} var opts = make(map[string]string) opts["tlsskipverify"] = "true" opts["proto"] = "http" var route1 = RouteDef{ Cmd: "route add", Service: "service1", Src: "app.com", Dst: "http://10.1.1.1:8080", Weight: 0.50, Tags: tags, Opts: opts, } var route2 = RouteDef{ Cmd: "route add", Service: "service1", Src: "app.com", Dst: "http://10.1.1.2:8080", Weight: 0.50, Tags: tags, Opts: opts, } var route3 = RouteDef{ Cmd: "route add", Service: "service2", Src: "app.com", Dst: "http://10.1.1.3:8080", Weight: 0.25, Tags: tags, Opts: opts, } routes = append(routes, route1) routes = append(routes, route2) routes = append(routes, route3) table, err := NewTableCustom(&routes) if err != nil { fmt.Printf("Got error from NewTableCustom - %s", err.Error()) t.FailNow() } tableString := table.String() if !strings.Contains(tableString, route1.Dst) { fmt.Printf("Table Missing Destination %s -- Table -- %s", route1.Dst, tableString) t.FailNow() } if !strings.Contains(tableString, route2.Dst) { fmt.Printf("Table Missing Destination %s -- Table -- %s", route1.Dst, tableString) t.FailNow() } if !strings.Contains(tableString, route3.Dst) { fmt.Printf("Table Missing Destination %s -- Table -- %s", route1.Dst, tableString) t.FailNow() } } func TestTable_Dump(t *testing.T) { s := ` route add svc / http://foo.com:800 route add svc /foo http://foo.com:900 route add svc abc.com/ http://foo.com:1000 ` tbl, err := NewTable(bytes.NewBufferString(s)) if err != nil { t.Fatal(err) } want := `+-- host= | |-- path=/foo | | +-- addr=foo.com:900 weight 1.00 slots 1/1 | +-- path=/ | +-- addr=foo.com:800 weight 1.00 slots 1/1 +-- host=abc.com +-- path=/ +-- addr=foo.com:1000 weight 1.00 slots 1/1 ` got := tbl.Dump() if want != got { t.Errorf("Unexpected Dump() output:\nwant:\n%s\ngot:\n%s\n", want, got) } } ================================================ FILE: route/target.go ================================================ package route import ( gkm "github.com/go-kit/kit/metrics" "net/http" "net/url" "strings" ) type Target struct { // Histogram measures throughput and latency of this target Timer gkm.Histogram // Counters for rx and tx RxCounter gkm.Counter TxCounter gkm.Counter // Opts is the raw options for the target. Opts map[string]string // URL is the endpoint the service instance listens on URL *url.URL // RedirectURL is the redirect target based on the request. // This is cached here to prevent multiple generations per request. RedirectURL *url.URL // accessRules is map of access information for the target. accessRules map[string][]interface{} // Transport allows for different types of transports Transport *http.Transport // Service is the name of the service the targetURL points to Service string // StripPath will be removed from the front of the outgoing // request path StripPath string // PrependPath will be added to the front of the outgoing // request path (after StripPath has been removed) PrependPath string // Host signifies what the proxy will set the Host header to. // The proxy does not modify the Host header by default. // When Host is set to 'dst' the proxy will use the host name // of the target host for the outgoing request. Host string // name of the auth handler for this target AuthScheme string // Tags are the list of tags for this target Tags []string // RedirectCode is the HTTP status code used for redirects. // When set to a value > 0 the client is redirected to the target url. RedirectCode int // FixedWeight is the weight assigned to this target. // If the value is 0 the targets weight is dynamic. FixedWeight float64 // Weight is the actual weight for this service in percent. Weight float64 // TLSSkipVerify disables certificate validation for upstream // TLS connections. TLSSkipVerify bool // ProxyProto enables PROXY Protocol on upstream connection ProxyProto bool } func (t *Target) BuildRedirectURL(requestURL *url.URL) { t.RedirectURL = &url.URL{ Scheme: t.URL.Scheme, Host: t.URL.Host, Path: t.URL.Path, RawPath: t.URL.Path, RawQuery: t.URL.RawQuery, } // treat case of $path not separated with a / from host if strings.HasSuffix(t.RedirectURL.Host, "$path") { t.RedirectURL.Host = t.RedirectURL.Host[:len(t.RedirectURL.Host)-len("$path")] t.RedirectURL.Path = "$path" } // remove / before $path in redirect url if strings.Contains(t.RedirectURL.Path, "/$path") { t.RedirectURL.Path = strings.Replace(t.RedirectURL.Path, "/$path", "$path", 1) t.RedirectURL.RawPath = strings.Replace(t.RedirectURL.RawPath, "/$path", "$path", 1) } // remove strip path, insert passed request path, set query if strings.Contains(t.RedirectURL.Path, "$path") { // set replacement paths replacePath := requestURL.Path var replaceRawPath string if requestURL.RawPath == "" { replaceRawPath = requestURL.Path } else { replaceRawPath = requestURL.RawPath } // strip path before replacement if t.StripPath != "" { replacePath = strings.TrimPrefix(replacePath, t.StripPath) replaceRawPath = strings.TrimPrefix(replaceRawPath, t.StripPath) } // add prepend path if t.PrependPath != "" { replacePath = t.PrependPath + replacePath replaceRawPath = t.PrependPath + replaceRawPath } // do path replacement t.RedirectURL.Path = strings.Replace(t.RedirectURL.Path, "$path", replacePath, 1) t.RedirectURL.RawPath = strings.Replace(t.RedirectURL.RawPath, "$path", replaceRawPath, 1) // set query if t.RedirectURL.RawQuery == "" && requestURL.RawQuery != "" { t.RedirectURL.RawQuery = requestURL.RawQuery } } if t.RedirectURL.Path == "" { t.RedirectURL.Path = "/" } t.RedirectURL.Host = strings.Replace(t.RedirectURL.Host, "$host", requestURL.Host, 1) } ================================================ FILE: route/target_test.go ================================================ package route import ( "bytes" "net/url" "testing" ) func TestTarget_BuildRedirectURL(t *testing.T) { type routeTest struct { req string want string } tests := []struct { route string tests []routeTest }{ { // simple absolute redirect route: "route add svc / http://bar.com/", tests: []routeTest{ {req: "/", want: "http://bar.com/"}, {req: "/abc", want: "http://bar.com/"}, {req: "/a/b/c", want: "http://bar.com/"}, {req: "/?aaa=1", want: "http://bar.com/"}, }, }, { // absolute redirect to deep path with query route: "route add svc / http://bar.com/a/b/c?foo=bar", tests: []routeTest{ {req: "/", want: "http://bar.com/a/b/c?foo=bar"}, {req: "/abc", want: "http://bar.com/a/b/c?foo=bar"}, {req: "/a/b/c", want: "http://bar.com/a/b/c?foo=bar"}, {req: "/?aaa=1", want: "http://bar.com/a/b/c?foo=bar"}, }, }, { // simple http -> https redirect with static path route: "route add redirect *:80/ https://$host/", tests: []routeTest{ {req: "/", want: "https://foo.com/"}, {req: "/abc", want: "https://foo.com/"}, {req: "/a/b/c", want: "https://foo.com/"}, {req: "/?aaa=1", want: "https://foo.com/"}, {req: "/abc/?aaa=1", want: "https://foo.com/"}, }, }, { // simple redirect to corresponding path route: "route add svc / http://bar.com/$path", tests: []routeTest{ {req: "/", want: "http://bar.com/"}, {req: "/abc", want: "http://bar.com/abc"}, {req: "/a/b/c", want: "http://bar.com/a/b/c"}, {req: "/?aaa=1", want: "http://bar.com/?aaa=1"}, {req: "/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, }, }, { // simple http -> https redirect to corresponding host & path route: "route add redirect *:80/ https://$host/$path", tests: []routeTest{ {req: "/", want: "https://foo.com/"}, {req: "/abc", want: "https://foo.com/abc"}, {req: "/a/b/c", want: "https://foo.com/a/b/c"}, {req: "/?aaa=1", want: "https://foo.com/?aaa=1"}, {req: "/abc/?aaa=1", want: "https://foo.com/abc/?aaa=1"}, }, }, { // simple redirect to corresponding path without / before $path route: "route add svc / http://bar.com$path", tests: []routeTest{ {req: "/", want: "http://bar.com/"}, {req: "/abc", want: "http://bar.com/abc"}, {req: "/a/b/c", want: "http://bar.com/a/b/c"}, {req: "/?aaa=1", want: "http://bar.com/?aaa=1"}, {req: "/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, }, }, { // simple http -> https redirect to corresponding host & path without / before $path route: "route add redirect *:80/ https://$host$path", tests: []routeTest{ {req: "/", want: "https://foo.com/"}, {req: "/abc", want: "https://foo.com/abc"}, {req: "/a/b/c", want: "https://foo.com/a/b/c"}, {req: "/?aaa=1", want: "https://foo.com/?aaa=1"}, {req: "/abc/?aaa=1", want: "https://foo.com/abc/?aaa=1"}, }, }, { // arbitrary subdir on target with $path at end route: "route add svc / http://bar.com/bbb/$path", tests: []routeTest{ {req: "/", want: "http://bar.com/bbb/"}, {req: "/abc", want: "http://bar.com/bbb/abc"}, {req: "/a/b/c", want: "http://bar.com/bbb/a/b/c"}, {req: "/?aaa=1", want: "http://bar.com/bbb/?aaa=1"}, {req: "/abc/?aaa=1", want: "http://bar.com/bbb/abc/?aaa=1"}, }, }, { // http -> https redir to corresponding host w/ arbitrary subdir on target with $path at end route: "route add redirect *:80/ https://$host/bbb/$path", tests: []routeTest{ {req: "/", want: "https://foo.com/bbb/"}, {req: "/abc", want: "https://foo.com/bbb/abc"}, {req: "/a/b/c", want: "https://foo.com/bbb/a/b/c"}, {req: "/?aaa=1", want: "https://foo.com/bbb/?aaa=1"}, {req: "/abc/?aaa=1", want: "https://foo.com/bbb/abc/?aaa=1"}, }, }, { // arbitrary subdir on target with $path at end but without / before $path route: "route add svc / http://bar.com/bbb$path", tests: []routeTest{ {req: "/", want: "http://bar.com/bbb/"}, {req: "/abc", want: "http://bar.com/bbb/abc"}, {req: "/a/b/c", want: "http://bar.com/bbb/a/b/c"}, {req: "/?aaa=1", want: "http://bar.com/bbb/?aaa=1"}, {req: "/abc/?aaa=1", want: "http://bar.com/bbb/abc/?aaa=1"}, }, }, { // http -> https redir to corresponding host w/ arbitrary subdir on target with $path at end but without / before $path route: "route add redirect *:80/ https://$host/bbb$path", tests: []routeTest{ {req: "/", want: "https://foo.com/bbb/"}, {req: "/abc", want: "https://foo.com/bbb/abc"}, {req: "/a/b/c", want: "https://foo.com/bbb/a/b/c"}, {req: "/?aaa=1", want: "https://foo.com/bbb/?aaa=1"}, {req: "/abc/?aaa=1", want: "https://foo.com/bbb/abc/?aaa=1"}, }, }, { // simple redirect to corresponding path with encoded char in path route: "route add svc / http://bar.com/$path", tests: []routeTest{ {req: "/%20", want: "http://bar.com/%20"}, {req: "/a%2fbc", want: "http://bar.com/a%2fbc"}, {req: "/a/b%22/c", want: "http://bar.com/a/b%22/c"}, {req: "/%2f/?aaa=1", want: "http://bar.com/%2f/?aaa=1"}, {req: "/%20/a%2f/", want: "http://bar.com/%20/a%2f/"}, }, }, { // strip prefix route: "route add svc /stripme http://bar.com/$path opts \"strip=/stripme\"", tests: []routeTest{ {req: "/stripme/", want: "http://bar.com/"}, {req: "/stripme/abc", want: "http://bar.com/abc"}, {req: "/stripme/a/b/c", want: "http://bar.com/a/b/c"}, {req: "/stripme/?aaa=1", want: "http://bar.com/?aaa=1"}, {req: "/stripme/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, }, }, { // strip prefix and redirect #824 route: "route add svc *:80/stripme https://bar.com/bbb$path opts \"strip=/stripme\"", tests: []routeTest{ {req: "/stripme", want: "https://bar.com/bbb"}, {req: "/stripme/abc", want: "https://bar.com/bbb/abc"}, {req: "/stripme/a/b/c", want: "https://bar.com/bbb/a/b/c"}, {req: "/stripme/?aaa=1", want: "https://bar.com/bbb/?aaa=1"}, {req: "/stripme/abc/?aaa=1", want: "https://bar.com/bbb/abc/?aaa=1"}, }, }, { // prepend prefix route: "route add svc / http://bar.com/$path opts \"prepend=/prefix\"", tests: []routeTest{ {req: "/", want: "http://bar.com/prefix/"}, {req: "/abc", want: "http://bar.com/prefix/abc"}, {req: "/a/b/c", want: "http://bar.com/prefix/a/b/c"}, {req: "/?aaa=1", want: "http://bar.com/prefix/?aaa=1"}, {req: "/abc/?aaa=1", want: "http://bar.com/prefix/abc/?aaa=1"}, }, }, { // strip & prepend prefix route: "route add svc / http://bar.com/$path opts \"prepend=/prefix strip=/stripme\"", tests: []routeTest{ {req: "/stripme/", want: "http://bar.com/prefix/"}, {req: "/stripme/abc", want: "http://bar.com/prefix/abc"}, {req: "/stripme/a/b/c", want: "http://bar.com/prefix/a/b/c"}, {req: "/stripme/?aaa=1", want: "http://bar.com/prefix/?aaa=1"}, {req: "/stripme/abc/?aaa=1", want: "http://bar.com/prefix/abc/?aaa=1"}, }, }, } firstRoute := func(tbl Table) *Route { for _, routes := range tbl { return routes[0] } return nil } for _, tt := range tests { tbl, _ := NewTable(bytes.NewBufferString(tt.route)) route := firstRoute(tbl) target := route.Targets[0] for _, rt := range tt.tests { reqURL, _ := url.Parse("http://foo.com" + rt.req) target.BuildRedirectURL(reqURL) if got := target.RedirectURL.String(); got != rt.want { t.Errorf("Got %s, wanted %s", got, rt.want) } } } } ================================================ FILE: transport/transport.go ================================================ package transport import ( "crypto/tls" "github.com/fabiolb/fabio/config" "net" "net/http" ) var ( cfg *config.Config = &config.Config{} ) func NewTransport(tlscfg *tls.Config) *http.Transport { return &http.Transport{ ResponseHeaderTimeout: cfg.Proxy.ResponseHeaderTimeout, IdleConnTimeout: cfg.Proxy.IdleConnTimeout, MaxIdleConnsPerHost: cfg.Proxy.MaxConn, Dial: (&net.Dialer{ Timeout: cfg.Proxy.DialTimeout, KeepAlive: cfg.Proxy.KeepAliveTimeout, }).Dial, TLSClientConfig: tlscfg, } } func SetConfig(ncfg *config.Config) { cfg = ncfg } ================================================ FILE: uuid/format.go ================================================ package uuid // Fast UUID formatting adapted from // https://github.com/m4rw3r/uuid/blob/master/uuid.go // halfbyte2hexchar contains an array of character values corresponding to // hexadecimal values for the position in the array, 0 to 15 (0x0-0xf, half-byte). var halfbyte2hexchar = []byte{ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, } // ToString formats raw UUID bytes as a standard UUID string func ToString(u [24]byte) string { /* It is a lot (~10x) faster to allocate a byte slice of specific size and then use a lookup table to write the characters to the byte-array and finally cast to string instead of using fmt.Sprintf() */ /* Slightly faster to not use make([]byte, 36), guessing either call overhead or slice-header overhead is the cause */ b := [36]byte{} for i, n := range []int{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34, } { b[n] = halfbyte2hexchar[(u[i]>>4)&0x0f] b[n+1] = halfbyte2hexchar[u[i]&0x0f] } b[8] = '-' b[13] = '-' b[18] = '-' b[23] = '-' /* Oddly does not seem to cause a memory allocation, internal data-array is most likely just moved over to the string-header: */ return string(b[:]) } ================================================ FILE: uuid/uuid.go ================================================ package uuid import ( "github.com/rogpeppe/fastuuid" ) var generator = fastuuid.MustNewGenerator() // NewUUID return UUID in string fromat func NewUUID() string { return ToString(generator.Next()) }