pax_global_header00006660000000000000000000000064132145505550014517gustar00rootroot0000000000000052 comment=c4d3c5243bcd81b116bde0c2c49b1307789378d7 prometheus-blackbox-exporter-0.11.0+ds/000077500000000000000000000000001321455055500200245ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/.dockerignore000066400000000000000000000000511321455055500224740ustar00rootroot00000000000000.build/ .tarballs/ !.build/linux-amd64/ prometheus-blackbox-exporter-0.11.0+ds/.github/000077500000000000000000000000001321455055500213645ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/.github/ISSUE_TEMPLATE.md000066400000000000000000000015641321455055500240770ustar00rootroot00000000000000 ### Host operating system: output of `uname -a` ### blackbox_exporter version: output of `blackbox_exporter -version` ### What is the blackbox.yml module config. ### What is the prometheus.yml scrape config. ### What logging output did you get from adding `&debug=true` to the probe URL? ### What did you do that produced an error? ### What did you expect to see? ### What did you see instead? prometheus-blackbox-exporter-0.11.0+ds/.gitignore000066400000000000000000000005161321455055500220160ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe dependencies-stamp /blackbox_exporter /.build /.release /.tarballs .deps *.tar.gz prometheus-blackbox-exporter-0.11.0+ds/.promu.yml000066400000000000000000000024421321455055500217710ustar00rootroot00000000000000repository: path: github.com/prometheus/blackbox_exporter build: flags: -a -tags netgo ldflags: | -X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Revision={{.Revision}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Branch={{.Branch}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} tarball: files: - blackbox.yml - LICENSE - NOTICE crossbuild: platforms: - linux/amd64 - linux/386 - darwin/amd64 - darwin/386 - windows/amd64 - windows/386 - freebsd/amd64 - freebsd/386 - openbsd/amd64 - openbsd/386 - netbsd/amd64 - netbsd/386 - dragonfly/amd64 - linux/arm - linux/arm64 - freebsd/arm - openbsd/arm - linux/mips64 - linux/mips64le # Temporarily deactivated as golang.org/x/sys does not have syscalls # implemented for that os/platform combination. #- netbsd/arm - linux/ppc64 - linux/ppc64le prometheus-blackbox-exporter-0.11.0+ds/.travis.yml000066400000000000000000000001661321455055500221400ustar00rootroot00000000000000sudo: false language: go go: - 1.9.x - 1.x go_import_path: github.com/prometheus/blackbox_exporter script: - make prometheus-blackbox-exporter-0.11.0+ds/CONFIGURATION.md000066400000000000000000000110771321455055500223630ustar00rootroot00000000000000# Blackbox exporter configuration The file is written in [YAML format](http://en.wikipedia.org/wiki/YAML), defined by the scheme described below. Brackets indicate that a parameter is optional. For non-list parameters the value is set to the specified default. Generic placeholders are defined as follows: * ``: a boolean that can take the values `true` or `false` * ``: a regular integer * ``: a duration matching the regular expression `[0-9]+(ms|[smhdwy])` * ``: a valid path in the current working directory * ``: a regular string * ``: a regular string that is a secret, such as a password * ``: a regular expression The other placeholders are specified separately. ### Module ```yml # The protocol over which the probe will take place (http, tcp, dns, icmp). prober: # How long the probe will wait before giving up. [ timeout: ] # The specific probe configuration - at most one of these should be specified. [ http: ] [ tcp: ] [ dns: ] [ icmp: ] ``` ### ```yml # Accepted status codes for this probe. Defaults to 2xx. [ valid_status_codes: , ... | default = 2xx ] # Accepted HTTP versions for this probe. [ valid_http_versions: , ... ] # The HTTP method the probe will use. [ method: | default = "GET" ] # The HTTP headers set for the probe. headers: [ : ... ] # Whether or not the probe will follow any redirects. [ no_follow_redirects: | default = false ] # Probe fails if SSL is present. [ fail_if_ssl: | default = false ] # Probe fails if SSL is not present. [ fail_if_not_ssl: | default = false ] # Probe fails if response matches regex. fail_if_matches_regexp: [ - , ... ] # Probe fails if response does not match regex. fail_if_not_matches_regexp: [ - , ... ] # Configuration for TLS protocol of HTTP probe. tls_config: [ ] # The HTTP basic authentication credentials for the targets. basic_auth: [ username: ] [ password: ] # The bearer token for the targets. [ bearer_token: ] # The bearer token file for the targets. [ bearer_token_file: ] # HTTP proxy server to use to connect to the targets. [ proxy_url: ] # The preferred IP protocol of the HTTP probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] # The body of the HTTP request used in probe. body: [ ] ``` ### ```yml # The preferred IP protocol of the TCP probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] # The query sent in the TCP probe and the expected associated response. # starttls upgrades TCP connection to TLS. query_response: [ - [ [ expect: ], [ send: ], [ starttls: ] ], ... ] # Whether or not TLS is used when the connection is initiated. [ tls: ] # Configuration for TLS protocol of TCP probe. tls_config: [ ] ``` ### ```yml # The preferred IP protocol of the DNS probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] [ transport_protocol: | default = "udp" ] # udp, tcp query_name: [ query_type: | default = "ANY" ] # List of valid response codes. valid_rcodes: [ - ... | default = "NOERROR" ] validate_answer_rrs: fail_if_matches_regexp: [ - , ... ] fail_if_not_matches_regexp: [ - , ... ] validate_authority_rrs: fail_if_matches_regexp: [ - , ... ] fail_if_not_matches_regexp: [ - , ... ] validate_additional_rrs: fail_if_matches_regexp: [ - , ... ] fail_if_not_matches_regexp: [ - , ... ] ``` ### ```yml # The preferred IP protocol of the ICMP probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] # Set the DF-bit in the IP-header. Only works with ip4 and on *nix systems. [ dont_fragment: | default = false ] # The size of the payload. [ payload_size: ] ``` ### ```yml # Disable target certificate validation. [ insecure_skip_verify: | default = false ] # The CA cert to use for the targets. [ ca_file: ] # The client cert file for the targets. [ cert_file: ] # The client key file for the targets. [ key_file: ] # Used to verify the hostname for the targets. [ server_name: ] ``` prometheus-blackbox-exporter-0.11.0+ds/Dockerfile000066400000000000000000000005411321455055500220160ustar00rootroot00000000000000FROM quay.io/prometheus/busybox:latest MAINTAINER The Prometheus Authors COPY blackbox_exporter /bin/blackbox_exporter COPY blackbox.yml /etc/blackbox_exporter/config.yml EXPOSE 9115 ENTRYPOINT [ "/bin/blackbox_exporter" ] CMD [ "--config.file=/etc/blackbox_exporter/config.yml" ] prometheus-blackbox-exporter-0.11.0+ds/LICENSE000066400000000000000000000261351321455055500210400ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed 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. prometheus-blackbox-exporter-0.11.0+ds/MAINTAINERS.md000066400000000000000000000000621321455055500221160ustar00rootroot00000000000000* Brian Brazil prometheus-blackbox-exporter-0.11.0+ds/Makefile000066400000000000000000000040441321455055500214660ustar00rootroot00000000000000# Copyright 2016 The Prometheus Authors # Licensed 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. GO := GO15VENDOREXPERIMENT=1 go FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) PROMU := $(FIRST_GOPATH)/bin/promu STATICCHECK := $(FIRST_GOPATH)/bin/staticcheck pkgs = $(shell $(GO) list ./... | grep -v /vendor/) PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_NAME ?= blackbox-exporter DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) all: format vet staticcheck build test style: @echo ">> checking code style" @! gofmt -d $(shell find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' test: @echo ">> running tests" @$(GO) test -short $(pkgs) format: @echo ">> formatting code" @$(GO) fmt $(pkgs) vet: @echo ">> vetting code" @$(GO) vet $(pkgs) staticcheck: $(STATICCHECK) @echo ">> running staticcheck" @$(STATICCHECK) $(pkgs) build: $(PROMU) @echo ">> building binaries" @$(PROMU) build --prefix $(PREFIX) tarball: $(PROMU) @echo ">> building release tarball" @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) docker: @echo ">> building docker image" @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . $(FIRST_GOPATH)/bin/promu promu: @GOOS= GOARCH= $(GO) get -u github.com/prometheus/promu $(FIRST_GOPATH)/bin/staticcheck: @GOOS= GOARCH= $(GO) get -u honnef.co/go/tools/cmd/staticcheck .PHONY: all style format build test vet tarball docker promu staticcheck $(FIRST_GOPATH)/bin/promu $(FIRST_GOPATH)/bin/staticcheck prometheus-blackbox-exporter-0.11.0+ds/NOTICE000066400000000000000000000001361321455055500207300ustar00rootroot00000000000000The Blackbox exporter for blackbox probing metrics Copyright 2012-2016 The Prometheus Authors prometheus-blackbox-exporter-0.11.0+ds/README.md000066400000000000000000000072741321455055500213150ustar00rootroot00000000000000# Blackbox exporter [![Build Status](https://travis-ci.org/prometheus/blackbox_exporter.svg)][travis] [![CircleCI](https://circleci.com/gh/prometheus/blackbox_exporter/tree/master.svg?style=shield)][circleci] [![Docker Repository on Quay](https://quay.io/repository/prometheus/blackbox-exporter/status)][quay] [![Docker Pulls](https://img.shields.io/docker/pulls/prom/blackbox-exporter.svg?maxAge=604800)][hub] The blackbox exporter allows blackbox probing of endpoints over HTTP, HTTPS, DNS, TCP and ICMP. ## Building and running ### Local Build make ./blackbox_exporter Visiting [http://localhost:9115/probe?target=google.com&module=http_2xx](http://localhost:9115/probe?target=google.com&module=http_2xx) will return metrics for a HTTP probe against google.com. The `probe_success` metric indicates if the probe succeeded. Adding a `debug=true` parameter will return debug information for that probe. ### Building with Docker docker build -t blackbox_exporter . docker run -d -p 9115:9115 --name blackbox_exporter -v `pwd`:/config blackbox_exporter --config.file=/config/blackbox.yml ## [Configuration](CONFIGURATION.md) Blackbox exporter is configured via a [configuration file](CONFIGURATION.md) and command-line flags (such as what configuration file to load, what port to listen on, and the logging format and level). Blackbox exporter can reload its configuration file at runtime. If the new configuration is not well-formed, the changes will not be applied. A configuration reload is triggered by sending a `SIGHUP` to the Blackbox exporter process or by sending a HTTP POST request to the `/-/reload` endpoint. To view all available command-line flags, run `./blackbox_exporter -h`. To specify which [configuration file](CONFIGURATION.md) to load, use the `--config.file` flag. Additionally, an [example configuration](example.yml) is also available. HTTP, HTTPS (via the `http` prober), DNS, TCP socket and ICMP (see permissions section) are currently supported. Additional modules can be defined to meet your needs. The timeout of each probe is automatically determined from the `scrape_timeout` in the [Prometheus config](https://prometheus.io/docs/operating/configuration/#configuration-file), slightly reduced to allow for network delays. This can be further limited by the `timeout` in the Blackbox exporter config file. If neither is specified, it defaults to 10 seconds. ## Prometheus Configuration The blackbox exporter needs to be passed the target as a parameter, this can be done with relabelling. Example config: ```yml scrape_configs: - job_name: 'blackbox' metrics_path: /probe params: module: [http_2xx] # Look for a HTTP 200 response. static_configs: - targets: - http://prometheus.io # Target to probe with http. - https://prometheus.io # Target to probe with https. - http://example.com:8080 # Target to probe with http on port 8080. relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: 127.0.0.1:9115 # Blackbox exporter. ``` ## Permissions The ICMP probe requires elevated privileges to function: * *Windows*: Administrator privileges are required. * *Linux*: root user _or_ `CAP_NET_RAW` capability is required. * Can be set by executing `setcap cap_net_raw+ep blackbox_exporter` * *BSD / OS X*: root user is required. [circleci]: https://circleci.com/gh/prometheus/blackbox_exporter [hub]: https://hub.docker.com/r/prom/blackbox-exporter/ [travis]: https://travis-ci.org/prometheus/blackbox_exporter [quay]: https://quay.io/repository/prometheus/blackbox-exporter prometheus-blackbox-exporter-0.11.0+ds/VERSION000066400000000000000000000000071321455055500210710ustar00rootroot000000000000000.11.0 prometheus-blackbox-exporter-0.11.0+ds/blackbox.yml000066400000000000000000000011771321455055500223420ustar00rootroot00000000000000modules: http_2xx: prober: http http: http_post_2xx: prober: http http: method: POST tcp_connect: prober: tcp pop3s_banner: prober: tcp tcp: query_response: - expect: "^+OK" tls: true tls_config: insecure_skip_verify: false ssh_banner: prober: tcp tcp: query_response: - expect: "^SSH-2.0-" irc_banner: prober: tcp tcp: query_response: - send: "NICK prober" - send: "USER prober prober prober :prober" - expect: "PING :([^ ]+)" send: "PONG ${1}" - expect: "^:[^ ]+ 001" icmp: prober: icmp prometheus-blackbox-exporter-0.11.0+ds/circle.yml000066400000000000000000000045411321455055500220140ustar00rootroot00000000000000machine: environment: DOCKER_IMAGE_NAME: prom/blackbox-exporter QUAY_IMAGE_NAME: quay.io/prometheus/blackbox-exporter DOCKER_TEST_IMAGE_NAME: quay.io/prometheus/golang-builder:1.9-base REPO_PATH: github.com/prometheus/blackbox_exporter pre: - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.9.1-circleci' - sudo chmod 0755 /usr/bin/docker - sudo curl -L 'https://github.com/aktau/github-release/releases/download/v0.6.2/linux-amd64-github-release.tar.bz2' | tar xvjf - --strip-components 3 -C $HOME/bin services: - docker dependencies: pre: - make promu - docker info override: - promu crossbuild - ln -s .build/linux-amd64/blackbox_exporter blackbox_exporter - | if [ -n "$CIRCLE_TAG" ]; then make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG else make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME fi post: - mkdir $CIRCLE_ARTIFACTS/binaries/ && cp -a .build/* $CIRCLE_ARTIFACTS/binaries/ - docker images test: override: - docker run --rm -t -v "$(pwd):/app" "${DOCKER_TEST_IMAGE_NAME}" -i "${REPO_PATH}" -T deployment: hub_branch: branch: master owner: prometheus commands: - docker login -e $DOCKER_EMAIL -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - docker login -e $QUAY_EMAIL -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - docker push $DOCKER_IMAGE_NAME - docker push $QUAY_IMAGE_NAME hub_tag: tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ owner: prometheus commands: - promu crossbuild tarballs - promu checksum .tarballs - promu release .tarballs - mkdir $CIRCLE_ARTIFACTS/releases/ && cp -a .tarballs/* $CIRCLE_ARTIFACTS/releases/ - docker login -e $DOCKER_EMAIL -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - docker login -e $QUAY_EMAIL -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - | if [[ "$CIRCLE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then docker tag "$DOCKER_IMAGE_NAME:$CIRCLE_TAG" "$DOCKER_IMAGE_NAME:latest" docker tag "$QUAY_IMAGE_NAME:$CIRCLE_TAG" "$QUAY_IMAGE_NAME:latest" fi - docker push $DOCKER_IMAGE_NAME - docker push $QUAY_IMAGE_NAME prometheus-blackbox-exporter-0.11.0+ds/config/000077500000000000000000000000001321455055500212715ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/config/config.go000066400000000000000000000164171321455055500230760ustar00rootroot00000000000000package config import ( "errors" "fmt" "io/ioutil" "runtime" "strings" "sync" "time" yaml "gopkg.in/yaml.v2" "github.com/prometheus/common/config" ) type Config struct { Modules map[string]Module `yaml:"modules"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type SafeConfig struct { sync.RWMutex C *Config } func (sc *SafeConfig) ReloadConfig(confFile string) (err error) { var c = &Config{} yamlFile, err := ioutil.ReadFile(confFile) if err != nil { return fmt.Errorf("Error reading config file: %s", err) } if err := yaml.Unmarshal(yamlFile, c); err != nil { return fmt.Errorf("Error parsing config file: %s", err) } sc.Lock() sc.C = c sc.Unlock() return nil } type Module struct { Prober string `yaml:"prober,omitempty"` Timeout time.Duration `yaml:"timeout,omitempty"` HTTP HTTPProbe `yaml:"http,omitempty"` TCP TCPProbe `yaml:"tcp,omitempty"` ICMP ICMPProbe `yaml:"icmp,omitempty"` DNS DNSProbe `yaml:"dns,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type HTTPProbe struct { // Defaults to 2xx. ValidStatusCodes []int `yaml:"valid_status_codes,omitempty"` ValidHTTPVersions []string `yaml:"valid_http_versions,omitempty"` PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` NoFollowRedirects bool `yaml:"no_follow_redirects,omitempty"` FailIfSSL bool `yaml:"fail_if_ssl,omitempty"` FailIfNotSSL bool `yaml:"fail_if_not_ssl,omitempty"` Method string `yaml:"method,omitempty"` Headers map[string]string `yaml:"headers,omitempty"` FailIfMatchesRegexp []string `yaml:"fail_if_matches_regexp,omitempty"` FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp,omitempty"` Body string `yaml:"body,omitempty"` HTTPClientConfig config.HTTPClientConfig `yaml:"http_client_config,inline"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type QueryResponse struct { Expect string `yaml:"expect,omitempty"` Send string `yaml:"send,omitempty"` StartTLS bool `yaml:"starttls,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type TCPProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` QueryResponse []QueryResponse `yaml:"query_response,omitempty"` TLS bool `yaml:"tls,omitempty"` TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type ICMPProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` // Defaults to "ip6". PayloadSize int `yaml:"payload_size,omitempty"` DontFragment bool `yaml:"dont_fragment,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type DNSProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` TransportProtocol string `yaml:"transport_protocol,omitempty"` QueryName string `yaml:"query_name,omitempty"` QueryType string `yaml:"query_type,omitempty"` // Defaults to ANY. ValidRcodes []string `yaml:"valid_rcodes,omitempty"` // Defaults to NOERROR. ValidateAnswer DNSRRValidator `yaml:"validate_answer_rrs,omitempty"` ValidateAuthority DNSRRValidator `yaml:"validate_authority_rrs,omitempty"` ValidateAdditional DNSRRValidator `yaml:"validate_additional_rrs,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } type DNSRRValidator struct { FailIfMatchesRegexp []string `yaml:"fail_if_matches_regexp,omitempty"` FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } func checkOverflow(m map[string]interface{}, ctx string) error { if len(m) > 0 { var keys []string for k := range m { keys = append(keys, k) } return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", ")) } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Config if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "config"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *Module) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Module if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "module"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *HTTPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain HTTPProbe if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "http probe"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *DNSProbe) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain DNSProbe if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "dns probe"); err != nil { return err } if s.QueryName == "" { return errors.New("Query name must be set for DNS module") } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *TCPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain TCPProbe if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "tcp probe"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *DNSRRValidator) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain DNSRRValidator if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "dns rr validator"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *ICMPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain ICMPProbe if err := unmarshal((*plain)(s)); err != nil { return err } if runtime.GOOS == "windows" && s.DontFragment { return errors.New("\"dont_fragment\" is not supported on windows platforms") } if err := checkOverflow(s.XXX, "icmp probe"); err != nil { return err } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (s *QueryResponse) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain QueryResponse if err := unmarshal((*plain)(s)); err != nil { return err } if err := checkOverflow(s.XXX, "query response"); err != nil { return err } return nil } prometheus-blackbox-exporter-0.11.0+ds/config/config_test.go000066400000000000000000000027351321455055500241330ustar00rootroot00000000000000package config import ( "strings" "testing" yaml "gopkg.in/yaml.v2" ) func TestLoadConfig(t *testing.T) { sc := &SafeConfig{ C: &Config{}, } err := sc.ReloadConfig("testdata/blackbox-good.yml") if err != nil { t.Errorf("Error loading config %v: %v", "blackbox.yml", err) } } func TestLoadBadConfigs(t *testing.T) { sc := &SafeConfig{ C: &Config{}, } tests := []struct { ConfigFile string ExpectedError string }{ { ConfigFile: "testdata/blackbox-bad.yml", ExpectedError: "Error parsing config file: unknown fields in dns probe: invalid_extra_field", }, { ConfigFile: "testdata/invalid-dns-module.yml", ExpectedError: "Error parsing config file: Query name must be set for DNS module", }, } for i, test := range tests { err := sc.ReloadConfig(test.ConfigFile) if err.Error() != test.ExpectedError { t.Errorf("In case %v:\nExpected:\n%v\nGot:\n%v", i, test.ExpectedError, err.Error()) } } } func TestHideConfigSecrets(t *testing.T) { sc := &SafeConfig{ C: &Config{}, } err := sc.ReloadConfig("testdata/blackbox-good.yml") if err != nil { t.Errorf("Error loading config %v: %v", "testdata/blackbox-good.yml", err) } // String method must not reveal authentication credentials. sc.RLock() c, err := yaml.Marshal(sc.C) sc.RUnlock() if err != nil { t.Errorf("Error marshalling config: %v", err) } if strings.Contains(string(c), "mysecret") { t.Fatal("config's String method reveals authentication credentials.") } } prometheus-blackbox-exporter-0.11.0+ds/config/testdata/000077500000000000000000000000001321455055500231025ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/config/testdata/blackbox-bad.yml000066400000000000000000000017101321455055500261350ustar00rootroot00000000000000modules: http_2xx: prober: http timeout: 5s http: http_post_2xx: prober: http timeout: 5s http: method: POST tcp_connect: prober: tcp timeout: 5s pop3s_banner: prober: tcp tcp: query_response: - expect: "^+OK" tls: true tls_config: insecure_skip_verify: false ssh_banner: prober: tcp timeout: 5s tcp: query_response: - expect: "^SSH-2.0-" irc_banner: prober: tcp timeout: 5s tcp: query_response: - send: "NICK prober" - send: "USER prober prober prober :prober" - expect: "PING :([^ ]+)" send: "PONG ${1}" - expect: "^:[^ ]+ 001" icmp_test: prober: icmp timeout: 5s icmp: preferred_ip_protocol: ip4 dns_test: prober: dns timeout: 5s dns: preferred_ip_protocol: ip6 validate_answer_rrs: fail_if_matches_regexp: [test] invalid_extra_field: value prometheus-blackbox-exporter-0.11.0+ds/config/testdata/blackbox-good.yml000066400000000000000000000025111321455055500263370ustar00rootroot00000000000000modules: http_2xx: prober: http timeout: 5s http: http_post_2xx: prober: http timeout: 5s http: method: POST basic_auth: username: "username" password: "mysecret" tcp_connect: prober: tcp timeout: 5s pop3s_banner: prober: tcp tcp: query_response: - expect: "^+OK" tls: true tls_config: insecure_skip_verify: false ssh_banner: prober: tcp timeout: 5s tcp: query_response: - expect: "^SSH-2.0-" smtp_starttls: prober: tcp timeout: 5s tcp: query_response: - expect: "^220 " - send: "EHLO prober" - expect: "^250-STARTTLS" - send: "STARTTLS" - expect: "^220" - starttls: true - send: "EHLO prober" - expect: "^250-AUTH" - send: "QUIT" irc_banner: prober: tcp timeout: 5s tcp: query_response: - send: "NICK prober" - send: "USER prober prober prober :prober" - expect: "PING :([^ ]+)" send: "PONG ${1}" - expect: "^:[^ ]+ 001" icmp_test: prober: icmp timeout: 5s icmp: preferred_ip_protocol: ip4 dns_test: prober: dns timeout: 5s dns: query_name: example.com preferred_ip_protocol: ip4 validate_answer_rrs: fail_if_matches_regexp: [test] prometheus-blackbox-exporter-0.11.0+ds/config/testdata/invalid-dns-module.yml000066400000000000000000000002411321455055500273150ustar00rootroot00000000000000modules: dns_test: prober: dns timeout: 5s dns: preferred_ip_protocol: ip6 validate_answer_rrs: fail_if_matches_regexp: [test] prometheus-blackbox-exporter-0.11.0+ds/example.yml000066400000000000000000000053121321455055500222030ustar00rootroot00000000000000modules: http_2xx_example: prober: http timeout: 5s http: valid_http_versions: ["HTTP/1.1", "HTTP/2"] valid_status_codes: [] # Defaults to 2xx method: GET headers: Host: vhost.example.com Accept-Language: en-US no_follow_redirects: false fail_if_ssl: false fail_if_not_ssl: false fail_if_matches_regexp: - "Could not connect to database" fail_if_not_matches_regexp: - "Download the latest version here" tls_config: insecure_skip_verify: false preferred_ip_protocol: "ip4" # defaults to "ip6" http_post_2xx: prober: http timeout: 5s http: method: POST headers: Content-Type: application/json body: '{}' http_basic_auth_example: prober: http timeout: 5s http: method: POST headers: Host: "login.example.com" basic_auth: username: "username" password: "mysecret" tls_connect: prober: tcp timeout: 5s tcp: tls: true tcp_connect_example: prober: tcp timeout: 5s imap_starttls: prober: tcp timeout: 5s tcp: query_response: - expect: "OK.*STARTTLS" - send: ". STARTTLS" - expect: "OK" - starttls: true - send: ". capability" - expect: "CAPABILITY IMAP4rev1" smtp_starttls: prober: tcp timeout: 5s tcp: query_response: - expect: "^220 ([^ ]+) ESMTP (.+)$" - send: "EHLO prober" - expect: "^250-STARTTLS" - send: "STARTTLS" - expect: "^220" - starttls: true - send: "EHLO prober" - expect: "^250-AUTH" - send: "QUIT" irc_banner_example: prober: tcp timeout: 5s tcp: query_response: - send: "NICK prober" - send: "USER prober prober prober :prober" - expect: "PING :([^ ]+)" send: "PONG ${1}" - expect: "^:[^ ]+ 001" icmp_example: prober: icmp timeout: 5s icmp: preferred_ip_protocol: "ip4" dns_udp_example: prober: dns timeout: 5s dns: query_name: "www.prometheus.io" query_type: "A" valid_rcodes: - NOERROR validate_answer_rrs: fail_if_matches_regexp: - ".*127.0.0.1" fail_if_not_matches_regexp: - "www.prometheus.io.\t300\tIN\tA\t127.0.0.1" validate_authority_rrs: fail_if_matches_regexp: - ".*127.0.0.1" validate_additional_rrs: fail_if_matches_regexp: - ".*127.0.0.1" dns_tcp_example: prober: dns dns: transport_protocol: "tcp" # defaults to "udp" preferred_ip_protocol: "ip4" # defaults to "ip6" query_name: "www.prometheus.io" prometheus-blackbox-exporter-0.11.0+ds/history.go000066400000000000000000000031671321455055500220630ustar00rootroot00000000000000// Copyright 2017 The Prometheus Authors // Licensed 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. package main import ( "sync" ) type result struct { id int64 moduleName string target string debugOutput string success bool } type resultHistory struct { mu sync.Mutex nextId int64 results []*result } // Add a result to the history. func (rh *resultHistory) Add(moduleName, target, debugOutput string, success bool) { rh.mu.Lock() defer rh.mu.Unlock() r := &result{ id: rh.nextId, moduleName: moduleName, target: target, debugOutput: debugOutput, success: success, } rh.nextId++ rh.results = append(rh.results, r) if len(rh.results) > 100 { results := make([]*result, len(rh.results)-1) copy(results, rh.results[1:]) rh.results = results } } // Return a list of all results. func (rh *resultHistory) List() []*result { rh.mu.Lock() defer rh.mu.Unlock() return rh.results[:] } // Return a given result. func (rh *resultHistory) Get(id int64) *result { rh.mu.Lock() defer rh.mu.Unlock() for _, r := range rh.results { if r.id == id { return r } } return nil } prometheus-blackbox-exporter-0.11.0+ds/main.go000066400000000000000000000236061321455055500213060ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package main import ( "bytes" "context" "fmt" "html" "net/http" _ "net/http/pprof" "os" "os/signal" "strconv" "syscall" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/expfmt" "github.com/prometheus/common/promlog" "github.com/prometheus/common/promlog/flag" "github.com/prometheus/common/version" "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/yaml.v2" "github.com/prometheus/blackbox_exporter/config" "github.com/prometheus/blackbox_exporter/prober" ) var ( sc = &config.SafeConfig{ C: &config.Config{}, } configFile = kingpin.Flag("config.file", "Blackbox exporter configuration file.").Default("blackbox.yml").String() listenAddress = kingpin.Flag("web.listen-address", "The address to listen on for HTTP requests.").Default(":9115").String() timeoutOffset = kingpin.Flag("timeout-offset", "Offset to subtract from timeout in seconds.").Default("0.5").Float64() Probers = map[string]prober.ProbeFn{ "http": prober.ProbeHTTP, "tcp": prober.ProbeTCP, "icmp": prober.ProbeICMP, "dns": prober.ProbeDNS, } ) func probeHandler(w http.ResponseWriter, r *http.Request, c *config.Config, logger log.Logger, rh *resultHistory) { moduleName := r.URL.Query().Get("module") if moduleName == "" { moduleName = "http_2xx" } module, ok := c.Modules[moduleName] if !ok { http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest) return } // If a timeout is configured via the Prometheus header, add it to the request. var timeoutSeconds float64 if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { var err error timeoutSeconds, err = strconv.ParseFloat(v, 64) if err != nil { http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError) return } } if timeoutSeconds == 0 { timeoutSeconds = 10 } if module.Timeout.Seconds() < timeoutSeconds && module.Timeout.Seconds() > 0 { timeoutSeconds = module.Timeout.Seconds() } timeoutSeconds -= *timeoutOffset ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second))) defer cancel() r = r.WithContext(ctx) probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_success", Help: "Displays whether or not the probe was a success", }) probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_duration_seconds", Help: "Returns how long the probe took to complete in seconds", }) params := r.URL.Query() target := params.Get("target") if target == "" { http.Error(w, "Target parameter is missing", http.StatusBadRequest) return } prober, ok := Probers[module.Prober] if !ok { http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest) return } sl := newScrapeLogger(logger, moduleName, target) level.Info(sl).Log("msg", "Beginning probe", "probe", module.Prober, "timeout_seconds", timeoutSeconds) start := time.Now() registry := prometheus.NewRegistry() registry.MustRegister(probeSuccessGauge) registry.MustRegister(probeDurationGauge) success := prober(ctx, target, module, registry, sl) duration := time.Since(start).Seconds() probeDurationGauge.Set(duration) if success { probeSuccessGauge.Set(1) level.Info(sl).Log("msg", "Probe succeeded", "duration_seconds", duration) } else { level.Error(sl).Log("msg", "Probe failed", "duration_seconds", duration) } debugOutput := DebugOutput(&module, &sl.buffer, registry) rh.Add(moduleName, target, debugOutput, success) if r.URL.Query().Get("debug") == "true" { w.Header().Set("Content-Type", "text/plain") w.Write([]byte(debugOutput)) return } h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) h.ServeHTTP(w, r) } type scrapeLogger struct { next log.Logger module string target string buffer bytes.Buffer bufferLogger log.Logger } func newScrapeLogger(logger log.Logger, module string, target string) *scrapeLogger { logger = log.With(logger, "module", module, "target", target) sl := &scrapeLogger{ next: logger, buffer: bytes.Buffer{}, } bl := log.NewLogfmtLogger(&sl.buffer) sl.bufferLogger = log.With(bl, "ts", log.DefaultTimestampUTC, "caller", log.Caller(6), "module", module, "target", target) return sl } func (sl scrapeLogger) Log(keyvals ...interface{}) error { sl.bufferLogger.Log(keyvals...) kvs := make([]interface{}, len(keyvals)) copy(kvs, keyvals) // Switch level to debug for application output. for i := 0; i < len(kvs); i += 2 { if kvs[i] == level.Key() { kvs[i+1] = level.DebugValue() } } return sl.next.Log(kvs...) } // Returns plaintext debug output for a probe. func DebugOutput(module *config.Module, logBuffer *bytes.Buffer, registry *prometheus.Registry) string { buf := &bytes.Buffer{} fmt.Fprintf(buf, "Logs for the probe:\n") logBuffer.WriteTo(buf) fmt.Fprintf(buf, "\n\n\nMetrics that would have been returned:\n") mfs, err := registry.Gather() if err != nil { fmt.Fprintf(buf, "Error gathering metrics: %s\n", err) } for _, mf := range mfs { expfmt.MetricFamilyToText(buf, mf) } fmt.Fprintf(buf, "\n\n\nModule configuration:\n") c, err := yaml.Marshal(module) if err != nil { fmt.Fprintf(buf, "Error marshalling config: %s\n", err) } buf.Write(c) return buf.String() } func init() { prometheus.MustRegister(version.NewCollector("blackbox_exporter")) } func main() { allowedLevel := promlog.AllowedLevel{} flag.AddFlags(kingpin.CommandLine, &allowedLevel) kingpin.Version(version.Print("blackbox_exporter")) kingpin.HelpFlag.Short('h') kingpin.Parse() logger := promlog.New(allowedLevel) rh := &resultHistory{} level.Info(logger).Log("msg", "Starting blackbox_exporter", "version", version.Info()) level.Info(logger).Log("msg", "Build context", version.BuildContext()) if err := sc.ReloadConfig(*configFile); err != nil { level.Error(logger).Log("msg", "Error loading config", "err", err) os.Exit(1) } level.Info(logger).Log("msg", "Loaded config file") hup := make(chan os.Signal) reloadCh := make(chan chan error) signal.Notify(hup, syscall.SIGHUP) go func() { for { select { case <-hup: if err := sc.ReloadConfig(*configFile); err != nil { level.Error(logger).Log("msg", "Error reloading config", "err", err) continue } level.Info(logger).Log("msg", "Reloaded config file") case rc := <-reloadCh: if err := sc.ReloadConfig(*configFile); err != nil { level.Error(logger).Log("msg", "Error reloading config", "err", err) rc <- err } else { level.Info(logger).Log("msg", "Reloaded config file") rc <- nil } } } }() http.HandleFunc("/-/reload", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, "This endpoint requires a POST request.\n") return } rc := make(chan error) reloadCh <- rc if err := <-rc; err != nil { http.Error(w, fmt.Sprintf("failed to reload config: %s", err), http.StatusInternalServerError) } }) http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/probe", func(w http.ResponseWriter, r *http.Request) { sc.Lock() conf := sc.C sc.Unlock() probeHandler(w, r, conf, logger, rh) }) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(` Blackbox Exporter

Blackbox Exporter

Probe prometheus.io for http_2xx

Debug probe prometheus.io for http_2xx

Metrics

Configuration

Recent Probes

`)) results := rh.List() for i := len(results) - 1; i >= 0; i-- { r := results[i] success := "Success" if !r.success { success = "Failure" } fmt.Fprintf(w, "", html.EscapeString(r.moduleName), html.EscapeString(r.target), success, r.id) } w.Write([]byte(`
ModuleTargetResultDebug
%s%s%sLogs
`)) }) http.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) if err != nil { http.Error(w, "Invalid probe id", 500) return } result := rh.Get(id) if result == nil { http.Error(w, "Probe id not found", 404) return } w.Header().Set("Content-Type", "text/plain") w.Write([]byte(result.debugOutput)) }) http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { sc.RLock() c, err := yaml.Marshal(sc.C) sc.RUnlock() if err != nil { level.Warn(logger).Log("msg", "Error marshalling configuration", "err", err) http.Error(w, err.Error(), 500) return } w.Header().Set("Content-Type", "text/plain") w.Write(c) }) level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress) if err := http.ListenAndServe(*listenAddress, nil); err != nil { level.Error(logger).Log("msg", "Error starting HTTP server", "err", err) os.Exit(1) } } prometheus-blackbox-exporter-0.11.0+ds/main_test.go000066400000000000000000000044771321455055500223520ustar00rootroot00000000000000package main import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/prometheus/blackbox_exporter/config" ) var c = &config.Config{ Modules: map[string]config.Module{ "http_2xx": config.Module{ Prober: "http", Timeout: 10 * time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ BearerToken: "mysecret", }, }, }, }, } func TestPrometheusTimeoutHTTP(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) })) defer ts.Close() req, err := http.NewRequest("GET", "?target="+ts.URL, nil) if err != nil { t.Fatal(err) } req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", "1") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) }) handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("probe request handler returned wrong status code: %v, want %v", status, http.StatusOK) } } func TestPrometheusConfigSecretsHidden(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) })) defer ts.Close() req, err := http.NewRequest("GET", "?debug=true&target="+ts.URL, nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probeHandler(w, r, c, log.NewNopLogger(), &resultHistory{}) }) handler.ServeHTTP(rr, req) body := rr.Body.String() if strings.Contains(body, "mysecret") { t.Errorf("Secret exposed in debug config output: %v", body) } if !strings.Contains(body, "") { t.Errorf("Hidden secret missing from debug config output: %v", body) } } func TestDebugOutputSecretsHidden(t *testing.T) { module := c.Modules["http_2xx"] out := DebugOutput(&module, &bytes.Buffer{}, prometheus.NewRegistry()) if strings.Contains(out, "mysecret") { t.Errorf("Secret exposed in debug output: %v", out) } if !strings.Contains(out, "") { t.Errorf("Hidden secret missing from debug output: %v", out) } } prometheus-blackbox-exporter-0.11.0+ds/prober/000077500000000000000000000000001321455055500213155ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/prober/dns.go000066400000000000000000000150171321455055500224340ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "context" "net" "regexp" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/blackbox_exporter/config" ) // validRRs checks a slice of RRs received from the server against a DNSRRValidator. func validRRs(rrs *[]dns.RR, v *config.DNSRRValidator, logger log.Logger) bool { // Fail the probe if there are no RRs of a given type, but a regexp match is required // (i.e. FailIfNotMatchesRegexp is set). if len(*rrs) == 0 && len(v.FailIfNotMatchesRegexp) > 0 { level.Error(logger).Log("msg", "fail_if_not_matches_regexp specified but no RRs returned") return false } for _, rr := range *rrs { level.Info(logger).Log("msg", "Validating RR", "rr", rr) for _, re := range v.FailIfMatchesRegexp { match, err := regexp.MatchString(re, rr.String()) if err != nil { level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err) return false } if match { level.Error(logger).Log("msg", "RR matched regexp", "regexp", re, "rr", rr) return false } } for _, re := range v.FailIfNotMatchesRegexp { match, err := regexp.MatchString(re, rr.String()) if err != nil { level.Error(logger).Log("msg", "Error matching regexp", "regexp", re, "err", err) return false } if !match { level.Error(logger).Log("msg", "RR did not match regexp", "regexp", re, "rr", rr) return false } } } return true } // validRcode checks rcode in the response against a list of valid rcodes. func validRcode(rcode int, valid []string, logger log.Logger) bool { var validRcodes []int // If no list of valid rcodes is specified, only NOERROR is considered valid. if valid == nil { validRcodes = append(validRcodes, dns.StringToRcode["NOERROR"]) } else { for _, rcode := range valid { rc, ok := dns.StringToRcode[rcode] if !ok { level.Error(logger).Log("msg", "Invalid rcode", "rcode", rcode, "known_rcode", dns.RcodeToString) return false } validRcodes = append(validRcodes, rc) } } for _, rc := range validRcodes { if rcode == rc { level.Info(logger).Log("msg", "Rcode is valid", "rcode", rcode, "string_rcode", dns.RcodeToString[rcode]) return true } } level.Error(logger).Log("msg", "Rcode is not one of the valid rcodes", "rcode", rcode, "string_rcode", dns.RcodeToString[rcode], "valid_rcodes", validRcodes) return false } func ProbeDNS(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) bool { var dialProtocol string probeDNSAnswerRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_dns_answer_rrs", Help: "Returns number of entries in the answer resource record list", }) probeDNSAuthorityRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_dns_authority_rrs", Help: "Returns number of entries in the authority resource record list", }) probeDNSAdditionalRRSGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_dns_additional_rrs", Help: "Returns number of entries in the additional resource record list", }) registry.MustRegister(probeDNSAnswerRRSGauge) registry.MustRegister(probeDNSAuthorityRRSGauge) registry.MustRegister(probeDNSAdditionalRRSGauge) var ip *net.IPAddr if module.DNS.TransportProtocol == "" { module.DNS.TransportProtocol = "udp" } if module.DNS.TransportProtocol == "udp" || module.DNS.TransportProtocol == "tcp" { targetAddr, port, err := net.SplitHostPort(target) if err != nil { // Target only contains host so fallback to default port and set targetAddr as target. port = "53" targetAddr = target } ip, _, err = chooseProtocol(module.DNS.PreferredIPProtocol, targetAddr, registry, logger) if err != nil { level.Error(logger).Log("msg", "Error resolving address", "err", err) return false } target = net.JoinHostPort(ip.String(), port) } else { level.Error(logger).Log("msg", "Configuration error: Expected transport protocol udp or tcp", "protocol", module.DNS.TransportProtocol) return false } if ip.IP.To4() == nil { dialProtocol = module.DNS.TransportProtocol + "6" } else { dialProtocol = module.DNS.TransportProtocol + "4" } client := new(dns.Client) client.Net = dialProtocol qt := dns.TypeANY if module.DNS.QueryType != "" { var ok bool qt, ok = dns.StringToType[module.DNS.QueryType] if !ok { level.Error(logger).Log("msg", "Invalid query type", "Type seen", module.DNS.QueryType, "Existing types", dns.TypeToString) return false } } msg := new(dns.Msg) msg.SetQuestion(dns.Fqdn(module.DNS.QueryName), qt) level.Info(logger).Log("msg", "Making DNS query", "target", target, "dial_protocol", dialProtocol, "query", module.DNS.QueryName, "type", qt) timeoutDeadline, _ := ctx.Deadline() client.Timeout = timeoutDeadline.Sub(time.Now()) response, _, err := client.Exchange(msg, target) if err != nil { level.Error(logger).Log("msg", "Error while sending a DNS query", "err", err) return false } level.Info(logger).Log("msg", "Got response", "response", response) probeDNSAnswerRRSGauge.Set(float64(len(response.Answer))) probeDNSAuthorityRRSGauge.Set(float64(len(response.Ns))) probeDNSAdditionalRRSGauge.Set(float64(len(response.Extra))) if !validRcode(response.Rcode, module.DNS.ValidRcodes, logger) { return false } level.Info(logger).Log("msg", "Validating Answer RRs") if !validRRs(&response.Answer, &module.DNS.ValidateAnswer, logger) { level.Error(logger).Log("msg", "Answer RRs validation failed") return false } level.Info(logger).Log("msg", "Validating Authority RRs") if !validRRs(&response.Ns, &module.DNS.ValidateAuthority, logger) { level.Error(logger).Log("msg", "Authority RRs validation failed") return false } level.Info(logger).Log("msg", "Validating Additional RRs") if !validRRs(&response.Extra, &module.DNS.ValidateAdditional, logger) { level.Error(logger).Log("msg", "Additional RRs validation failed") return false } return true } prometheus-blackbox-exporter-0.11.0+ds/prober/dns_test.go000066400000000000000000000323651321455055500235000ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "context" "net" "runtime" "testing" "time" "github.com/go-kit/kit/log" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/blackbox_exporter/config" ) var PROTOCOLS = [...]string{"udp", "tcp"} // startDNSServer starts a DNS server with a given handler function on a random port. // Returns the Server object itself as well as the net.Addr corresponding to the server port. func startDNSServer(protocol string, handler func(dns.ResponseWriter, *dns.Msg)) (*dns.Server, net.Addr) { h := dns.NewServeMux() h.HandleFunc(".", handler) server := &dns.Server{Addr: ":0", Net: protocol, Handler: h} go server.ListenAndServe() // Wait until PacketConn becomes available, but give up after 1 second. for i := 0; server.PacketConn == nil && i < 200; i++ { if protocol == "tcp" && server.Listener != nil { break } if protocol == "udp" && server.PacketConn != nil { break } time.Sleep(5 * time.Millisecond) } if protocol == "tcp" { return server, server.Listener.Addr() } return server, server.PacketConn.LocalAddr() } func recursiveDNSHandler(w dns.ResponseWriter, r *dns.Msg) { m := new(dns.Msg) m.SetReply(r) answers := []string{ "example.com. 3600 IN A 127.0.0.1", "example.com. 3600 IN A 127.0.0.2", } for _, rr := range answers { a, err := dns.NewRR(rr) if err != nil { panic(err) } m.Answer = append(m.Answer, a) } if err := w.WriteMsg(m); err != nil { panic(err) } } func TestRecursiveDNSResponse(t *testing.T) { tests := []struct { Probe config.DNSProbe ShouldSucceed bool }{ { config.DNSProbe{ QueryName: "example.com", }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidRcodes: []string{"SERVFAIL", "NXDOMAIN"}, }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidateAnswer: config.DNSRRValidator{ FailIfMatchesRegexp: []string{".*7200.*"}, FailIfNotMatchesRegexp: []string{".*3600.*"}, }, }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidateAuthority: config.DNSRRValidator{ FailIfMatchesRegexp: []string{".*7200.*"}, }, }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidateAdditional: config.DNSRRValidator{ FailIfNotMatchesRegexp: []string{".*3600.*"}, }, }, false, }, } for _, protocol := range PROTOCOLS { server, addr := startDNSServer(protocol, recursiveDNSHandler) defer server.Shutdown() for i, test := range tests { test.Probe.TransportProtocol = protocol registry := prometheus.NewPedanticRegistry() registry.Gather() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeDNS(testCTX, addr.String(), config.Module{Timeout: time.Second, DNS: test.Probe}, registry, log.NewNopLogger()) if result != test.ShouldSucceed { t.Fatalf("Test %d had unexpected result: %v", i, result) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_dns_answer_rrs": 2, "probe_dns_authority_rrs": 0, "probe_dns_additional_rrs": 0, } checkRegistryResults(expectedResults, mfs, t) } } } func authoritativeDNSHandler(w dns.ResponseWriter, r *dns.Msg) { m := new(dns.Msg) m.SetReply(r) a, err := dns.NewRR("example.com. 3600 IN A 127.0.0.1") if err != nil { panic(err) } m.Answer = append(m.Answer, a) authority := []string{ "example.com. 7200 IN NS ns1.isp.net.", "example.com. 7200 IN NS ns2.isp.net.", } for _, rr := range authority { a, err := dns.NewRR(rr) if err != nil { panic(err) } m.Ns = append(m.Ns, a) } additional := []string{ "ns1.isp.net. 7200 IN A 127.0.0.1", "ns1.isp.net. 7200 IN AAAA ::1", "ns2.isp.net. 7200 IN A 127.0.0.2", } for _, rr := range additional { a, err := dns.NewRR(rr) if err != nil { panic(err) } m.Extra = append(m.Extra, a) } if err := w.WriteMsg(m); err != nil { panic(err) } } func TestAuthoritativeDNSResponse(t *testing.T) { tests := []struct { Probe config.DNSProbe ShouldSucceed bool }{ { config.DNSProbe{ QueryName: "example.com", }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidRcodes: []string{"SERVFAIL", "NXDOMAIN"}, }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidateAnswer: config.DNSRRValidator{ FailIfMatchesRegexp: []string{".*3600.*"}, FailIfNotMatchesRegexp: []string{".*3600.*"}, }, }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidateAnswer: config.DNSRRValidator{ FailIfMatchesRegexp: []string{".*7200.*"}, FailIfNotMatchesRegexp: []string{".*7200.*"}, }, }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidateAuthority: config.DNSRRValidator{ FailIfNotMatchesRegexp: []string{"ns.*.isp.net"}, }, }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidateAdditional: config.DNSRRValidator{ FailIfNotMatchesRegexp: []string{"^ns.*.isp"}, }, }, true, }, { config.DNSProbe{ QueryName: "example.com", ValidateAdditional: config.DNSRRValidator{ FailIfMatchesRegexp: []string{"^ns.*.isp"}, }, }, false, }, } for _, protocol := range PROTOCOLS { server, addr := startDNSServer(protocol, authoritativeDNSHandler) defer server.Shutdown() for i, test := range tests { test.Probe.TransportProtocol = protocol registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeDNS(testCTX, addr.String(), config.Module{Timeout: time.Second, DNS: test.Probe}, registry, log.NewNopLogger()) if result != test.ShouldSucceed { t.Fatalf("Test %d had unexpected result: %v", i, result) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_dns_answer_rrs": 1, "probe_dns_authority_rrs": 2, "probe_dns_additional_rrs": 3, } checkRegistryResults(expectedResults, mfs, t) } } } func TestServfailDNSResponse(t *testing.T) { tests := []struct { Probe config.DNSProbe ShouldSucceed bool }{ { config.DNSProbe{ QueryName: "example.com", }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidRcodes: []string{"SERVFAIL", "NXDOMAIN"}, }, true, }, { config.DNSProbe{ QueryName: "example.com", QueryType: "NOT_A_VALID_QUERY_TYPE", }, false, }, { config.DNSProbe{ QueryName: "example.com", ValidRcodes: []string{"NOT_A_VALID_RCODE"}, }, false, }, } for _, protocol := range PROTOCOLS { // dns.HandleFailed returns SERVFAIL on everything server, addr := startDNSServer(protocol, dns.HandleFailed) defer server.Shutdown() for i, test := range tests { test.Probe.TransportProtocol = protocol registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeDNS(testCTX, addr.String(), config.Module{Timeout: time.Second, DNS: test.Probe}, registry, log.NewNopLogger()) if result != test.ShouldSucceed { t.Fatalf("Test %d had unexpected result: %v", i, result) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_dns_answer_rrs": 0, "probe_dns_authority_rrs": 0, "probe_dns_additional_rrs": 0, } checkRegistryResults(expectedResults, mfs, t) } } } func TestDNSProtocol(t *testing.T) { // This test assumes that listening TCP listens both IPv6 and IPv4 traffic and // localhost resolves to both 127.0.0.1 and ::1. we must skip the test if either // of these isn't true. This should be true for modern Linux systems. if runtime.GOOS == "dragonfly" || runtime.GOOS == "openbsd" { t.Skip("IPv6 socket isn't able to accept IPv4 traffic in the system.") } _, err := net.ResolveIPAddr("ip6", "localhost") if err != nil { t.Skip("\"localhost\" doesn't resolve to ::1.") } for _, protocol := range PROTOCOLS { server, addr := startDNSServer(protocol, recursiveDNSHandler) defer server.Shutdown() _, port, _ := net.SplitHostPort(addr.String()) // Force IPv4 module := config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", TransportProtocol: protocol, PreferredIPProtocol: "ip4", }, } registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("DNS protocol: \"%v4\" connection test failed, expected success.", protocol) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_ip_protocol": 4, } checkRegistryResults(expectedResults, mfs, t) // Force IPv6 module = config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", TransportProtocol: protocol, PreferredIPProtocol: "ip6", }, } registry = prometheus.NewRegistry() testCTX, cancel = context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result = ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("DNS protocol: \"%v6\" connection test failed, expected success.", protocol) } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // Prefer IPv6 module = config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", TransportProtocol: protocol, PreferredIPProtocol: "ip6", }, } registry = prometheus.NewRegistry() testCTX, cancel = context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result = ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("DNS protocol: \"%v\", preferred \"ip6\" connection test failed, expected success.", protocol) } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // Prefer IPv4 module = config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", TransportProtocol: protocol, PreferredIPProtocol: "ip4", }, } registry = prometheus.NewRegistry() testCTX, cancel = context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result = ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("DNS protocol: \"%v\", preferred \"ip4\" connection test failed, expected success.", protocol) } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 4, } checkRegistryResults(expectedResults, mfs, t) // Prefer none module = config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", TransportProtocol: protocol, }, } registry = prometheus.NewRegistry() testCTX, cancel = context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result = ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("DNS protocol: \"%v\" connection test failed, expected success.", protocol) } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // No protocol module = config.Module{ Timeout: time.Second, DNS: config.DNSProbe{ QueryName: "example.com", }, } registry = prometheus.NewRegistry() testCTX, cancel = context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result = ProbeDNS(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if protocol == "udp" { if !result { t.Fatalf("DNS test connection with protocol %s failed, expected success.", protocol) } } else { if result { t.Fatalf("DNS test connection with protocol %s succeeded, expected failure.", protocol) } } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) } } prometheus-blackbox-exporter-0.11.0+ds/prober/http.go000066400000000000000000000304101321455055500226210ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "context" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/cookiejar" "net/http/httptrace" "net/url" "regexp" "strconv" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "golang.org/x/net/publicsuffix" "github.com/prometheus/blackbox_exporter/config" ) func matchRegularExpressions(reader io.Reader, httpConfig config.HTTPProbe, logger log.Logger) bool { body, err := ioutil.ReadAll(reader) if err != nil { level.Error(logger).Log("msg", "Error reading HTTP body", "err", err) return false } for _, expression := range httpConfig.FailIfMatchesRegexp { re, err := regexp.Compile(expression) if err != nil { level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err) return false } if re.Match(body) { level.Error(logger).Log("msg", "Body matched regular expression", "regexp", expression) return false } } for _, expression := range httpConfig.FailIfNotMatchesRegexp { re, err := regexp.Compile(expression) if err != nil { level.Error(logger).Log("msg", "Could not compile regular expression", "regexp", expression, "err", err) return false } if !re.Match(body) { level.Error(logger).Log("msg", "Body did not match regular expression", "regexp", expression) return false } } return true } // roundTripTrace holds timings for a single HTTP roundtrip. type roundTripTrace struct { tls bool start time.Time dnsDone time.Time connectDone time.Time gotConn time.Time responseStart time.Time end time.Time } // transport is a custom transport keeping traces for each HTTP roundtrip. type transport struct { Transport http.RoundTripper logger log.Logger traces []*roundTripTrace current *roundTripTrace } func newTransport(rt http.RoundTripper, logger log.Logger) *transport { return &transport{ Transport: rt, logger: logger, traces: []*roundTripTrace{}, } } // RoundTrip switches to a new trace, then runs embedded RoundTripper. func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { trace := &roundTripTrace{} if req.URL.Scheme == "https" { trace.tls = true } t.current = trace t.traces = append(t.traces, trace) defer func() { trace.end = time.Now() }() return t.Transport.RoundTrip(req) } func (t *transport) DNSStart(_ httptrace.DNSStartInfo) { t.current.start = time.Now() } func (t *transport) DNSDone(_ httptrace.DNSDoneInfo) { t.current.dnsDone = time.Now() } func (ts *transport) ConnectStart(_, _ string) { t := ts.current // No DNS resolution because we connected to IP directly. if t.dnsDone.IsZero() { t.start = time.Now() t.dnsDone = t.start } } func (t *transport) ConnectDone(net, addr string, err error) { t.current.connectDone = time.Now() } func (t *transport) GotConn(_ httptrace.GotConnInfo) { t.current.gotConn = time.Now() } func (t *transport) GotFirstResponseByte() { t.current.responseStart = time.Now() } func ProbeHTTP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) { var redirects int var ( durationGaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "probe_http_duration_seconds", Help: "Duration of http request by phase, summed over all redirects", }, []string{"phase"}) contentLengthGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "content_length", Help: "Length of http content response", }) redirectsGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_http_redirects", Help: "The number of redirects", }) isSSLGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_http_ssl", Help: "Indicates if SSL was used for the final redirect", }) statusCodeGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_http_status_code", Help: "Response HTTP status code", }) probeSSLEarliestCertExpiryGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_ssl_earliest_cert_expiry", Help: "Returns earliest SSL cert expiry in unixtime", }) probeHTTPVersionGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_http_version", Help: "Returns the version of HTTP of the probe response", }) probeFailedDueToRegex = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_failed_due_to_regex", Help: "Indicates if probe failed due to regex", }) ) for _, lv := range []string{"resolve", "connect", "tls", "processing", "transfer"} { durationGaugeVec.WithLabelValues(lv) } registry.MustRegister(durationGaugeVec) registry.MustRegister(contentLengthGauge) registry.MustRegister(redirectsGauge) registry.MustRegister(isSSLGauge) registry.MustRegister(statusCodeGauge) registry.MustRegister(probeHTTPVersionGauge) registry.MustRegister(probeFailedDueToRegex) httpConfig := module.HTTP if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") { target = "http://" + target } targetURL, err := url.Parse(target) if err != nil { level.Error(logger).Log("msg", "Could not parse target URL", "err", err) return false } targetHost, targetPort, err := net.SplitHostPort(targetURL.Host) // If split fails, assuming it's a hostname without port part. if err != nil { targetHost = targetURL.Host } ip, lookupTime, err := chooseProtocol(module.HTTP.PreferredIPProtocol, targetHost, registry, logger) if err != nil { level.Error(logger).Log("msg", "Error resolving address", "err", err) return false } durationGaugeVec.WithLabelValues("resolve").Add(lookupTime) httpClientConfig := module.HTTP.HTTPClientConfig if len(httpClientConfig.TLSConfig.ServerName) == 0 { // If there is no `server_name` in tls_config, use // the hostname of the target. httpClientConfig.TLSConfig.ServerName = targetHost } client, err := pconfig.NewHTTPClientFromConfig(&httpClientConfig) if err != nil { level.Error(logger).Log("msg", "Error generating HTTP client", "err", err) return false } jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { level.Error(logger).Log("msg", "Error generating cookiejar", "err", err) return false } client.Jar = jar // Inject transport that tracks trace for each redirect. tt := newTransport(client.Transport, logger) client.Transport = tt client.CheckRedirect = func(r *http.Request, via []*http.Request) error { level.Info(logger).Log("msg", "Received redirect", "url", r.URL.String()) redirects = len(via) if redirects > 10 || httpConfig.NoFollowRedirects { level.Info(logger).Log("msg", "Not following redirect") return errors.New("Don't follow redirects") } return nil } if httpConfig.Method == "" { httpConfig.Method = "GET" } // Replace the host field in the URL with the IP we resolved. origHost := targetURL.Host if targetPort == "" { targetURL.Host = "[" + ip.String() + "]" } else { targetURL.Host = net.JoinHostPort(ip.String(), targetPort) } var body io.Reader // If a body is configured, add it to the request. if httpConfig.Body != "" { body = strings.NewReader(httpConfig.Body) } request, err := http.NewRequest(httpConfig.Method, targetURL.String(), body) request.Host = origHost request = request.WithContext(ctx) if err != nil { level.Error(logger).Log("msg", "Error creating request", "err", err) return } for key, value := range httpConfig.Headers { if strings.Title(key) == "Host" { request.Host = value continue } request.Header.Set(key, value) } level.Info(logger).Log("msg", "Making HTTP request", "url", request.URL.String(), "host", request.Host) trace := &httptrace.ClientTrace{ DNSStart: tt.DNSStart, DNSDone: tt.DNSDone, ConnectStart: tt.ConnectStart, ConnectDone: tt.ConnectDone, GotConn: tt.GotConn, GotFirstResponseByte: tt.GotFirstResponseByte, } request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) resp, err := client.Do(request) // Err won't be nil if redirects were turned off. See https://github.com/golang/go/issues/3795 if err != nil && resp == nil { level.Error(logger).Log("msg", "Error for HTTP request", "err", err) } else { defer resp.Body.Close() level.Info(logger).Log("msg", "Received HTTP response", "status_code", resp.StatusCode) if len(httpConfig.ValidStatusCodes) != 0 { for _, code := range httpConfig.ValidStatusCodes { if resp.StatusCode == code { success = true break } } if !success { level.Info(logger).Log("msg", "Invalid HTTP response status code", "status_code", resp.StatusCode, "valid_status_codes", fmt.Sprintf("%v", httpConfig.ValidStatusCodes)) } } else if 200 <= resp.StatusCode && resp.StatusCode < 300 { success = true } else { level.Info(logger).Log("msg", "Invalid HTTP response status code, wanted 2xx", "status_code", resp.StatusCode) } if success && (len(httpConfig.FailIfMatchesRegexp) > 0 || len(httpConfig.FailIfNotMatchesRegexp) > 0) { success = matchRegularExpressions(resp.Body, httpConfig, logger) if success { probeFailedDueToRegex.Set(0) } else { probeFailedDueToRegex.Set(1) } } var httpVersionNumber float64 httpVersionNumber, err = strconv.ParseFloat(strings.TrimPrefix(resp.Proto, "HTTP/"), 64) if err != nil { level.Error(logger).Log("msg", "Error parsing version number from HTTP version", "err", err) } probeHTTPVersionGauge.Set(httpVersionNumber) if len(httpConfig.ValidHTTPVersions) != 0 { found := false for _, version := range httpConfig.ValidHTTPVersions { if version == resp.Proto { found = true break } } if !found { level.Error(logger).Log("msg", "Invalid HTTP version number", "version", httpVersionNumber) success = false } } } if resp == nil { resp = &http.Response{} } for i, trace := range tt.traces { level.Info(logger).Log( "msg", "Response timings for roundtrip", "roundtrip", i, "start", trace.start, "dnsDone", trace.dnsDone, "connectDone", trace.connectDone, "gotConn", trace.gotConn, "responseStart", trace.responseStart, "end", trace.end, ) // We get the duration for the first request from chooseProtocol. if i != 0 { durationGaugeVec.WithLabelValues("resolve").Add(trace.dnsDone.Sub(trace.start).Seconds()) } // Continue here if we never got a connection because a request failed. if trace.gotConn.IsZero() { continue } if trace.tls { // dnsDone must be set if gotConn was set. durationGaugeVec.WithLabelValues("connect").Add(trace.connectDone.Sub(trace.dnsDone).Seconds()) durationGaugeVec.WithLabelValues("tls").Add(trace.gotConn.Sub(trace.dnsDone).Seconds()) } else { durationGaugeVec.WithLabelValues("connect").Add(trace.gotConn.Sub(trace.dnsDone).Seconds()) } // Continue here if we never got a response from the server. if trace.responseStart.IsZero() { continue } durationGaugeVec.WithLabelValues("processing").Add(trace.responseStart.Sub(trace.gotConn).Seconds()) durationGaugeVec.WithLabelValues("transfer").Add(trace.end.Sub(trace.responseStart).Seconds()) } if resp.TLS != nil { isSSLGauge.Set(float64(1)) registry.MustRegister(probeSSLEarliestCertExpiryGauge) probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix())) if httpConfig.FailIfSSL { level.Error(logger).Log("msg", "Final request was over SSL") success = false } } else if httpConfig.FailIfNotSSL { level.Error(logger).Log("msg", "Final request was not over SSL") success = false } statusCodeGauge.Set(float64(resp.StatusCode)) contentLengthGauge.Set(float64(resp.ContentLength)) redirectsGauge.Set(float64(redirects)) return } prometheus-blackbox-exporter-0.11.0+ds/prober/http_test.go000066400000000000000000000502661321455055500236730ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed 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. package prober import ( "context" "crypto/tls" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/prometheus/blackbox_exporter/config" ) func TestHTTPStatusCodes(t *testing.T) { tests := []struct { StatusCode int ValidStatusCodes []int ShouldSucceed bool }{ {200, []int{}, true}, {201, []int{}, true}, {299, []int{}, true}, {300, []int{}, false}, {404, []int{}, false}, {404, []int{200, 404}, true}, {200, []int{200, 404}, true}, {201, []int{200, 404}, false}, {404, []int{404}, true}, {200, []int{404}, false}, } for i, test := range tests { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.StatusCode) })) defer ts.Close() registry := prometheus.NewRegistry() recorder := httptest.NewRecorder() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ValidStatusCodes: test.ValidStatusCodes}}, registry, log.NewNopLogger()) body := recorder.Body.String() if result != test.ShouldSucceed { t.Fatalf("Test %d had unexpected result: %s", i, body) } } } func TestValidHTTPVersion(t *testing.T) { tests := []struct { ValidHTTPVersions []string ShouldSucceed bool }{ {[]string{}, true}, {[]string{"HTTP/1.1"}, true}, {[]string{"HTTP/1.1", "HTTP/2"}, true}, {[]string{"HTTP/2"}, false}, } for i, test := range tests { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() result := ProbeHTTP(context.Background(), ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ ValidHTTPVersions: test.ValidHTTPVersions, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if result != test.ShouldSucceed { t.Fatalf("Test %v had unexpected result: %s", i, body) } } } func TestRedirectFollowed(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.Redirect(w, r, "/noredirect", http.StatusFound) } })) defer ts.Close() // Follow redirect, should succeed with 200. recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{}}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Redirect test failed unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_http_redirects": 1, } checkRegistryResults(expectedResults, mfs, t) } func TestRedirectNotFollowed(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/noredirect", http.StatusFound) })) defer ts.Close() // Follow redirect, should succeed with 200. recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{NoFollowRedirects: true, ValidStatusCodes: []int{302}}}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Redirect test failed unexpectedly, got %s", body) } } func TestPost(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusBadRequest) } })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{Method: "POST"}}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Post test failed unexpectedly, got %s", body) } } func TestBasicAuth(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{InsecureSkipVerify: false}, BasicAuth: &pconfig.BasicAuth{Username: "username", Password: "password"}, }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("HTTP probe failed, got %s", body) } } func TestBearerToken(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ BearerToken: pconfig.Secret("mysecret"), }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("HTTP probe failed, got %s", body) } } func TestFailIfNotSSL(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfNotSSL: true}}, registry, log.NewNopLogger()) body := recorder.Body.String() if result { t.Fatalf("Fail if not SSL test suceeded unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_http_ssl": 0, } checkRegistryResults(expectedResults, mfs, t) } func TestFailIfMatchesRegexp(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Bad news: could not connect to database server") })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger()) body := recorder.Body.String() if result { t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_failed_due_to_regex": 1, } checkRegistryResults(expectedResults, mfs, t) ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Download the latest version here") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if !result { t.Fatalf("Regexp test failed unexpectedly, got %s", body) } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_failed_due_to_regex": 0, } checkRegistryResults(expectedResults, mfs, t) // With multiple regexps configured, verify that any matching regexp causes // the probe to fail, but probes succeed when no regexp matches. ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "internal error") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if result { t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfMatchesRegexp: []string{"could not connect to database", "internal error"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if !result { t.Fatalf("Regexp test failed unexpectedly, got %s", body) } } func TestFailIfNotMatchesRegexp(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Bad news: could not connect to database server") })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger()) body := recorder.Body.String() if result { t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Download the latest version here") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if !result { t.Fatalf("Regexp test failed unexpectedly, got %s", body) } // With multiple regexps configured, verify that any non-matching regexp // causes the probe to fail, but probes succeed when all regexps match. ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Download the latest version here") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if result { t.Fatalf("Regexp test succeeded unexpectedly, got %s", body) } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Download the latest version here. Copyright 2015 Test Inc.") })) defer ts.Close() recorder = httptest.NewRecorder() registry = prometheus.NewRegistry() result = ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{FailIfNotMatchesRegexp: []string{"Download the latest version here", "Copyright 2015"}}}, registry, log.NewNopLogger()) body = recorder.Body.String() if !result { t.Fatalf("Regexp test failed unexpectedly, got %s", body) } } func TestHTTPHeaders(t *testing.T) { headers := map[string]string{ "Host": "my-secret-vhost.com", "User-Agent": "unsuspicious user", "Accept-Language": "en-US", } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for key, value := range headers { if strings.Title(key) == "Host" { if r.Host != value { t.Errorf("Unexpected host: expected %q, got %q.", value, r.Host) } continue } if got := r.Header.Get(key); got != value { t.Errorf("Unexpected value of header %q: expected %q, got %q", key, value, got) } } w.WriteHeader(http.StatusOK) })) defer ts.Close() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ Headers: headers, }}, registry, log.NewNopLogger()) if !result { t.Fatalf("Probe failed unexpectedly.") } } func TestFailIfSelfSignedCA(t *testing.T) { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{InsecureSkipVerify: false}, }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if result { t.Fatalf("Fail if selfsigned CA test suceeded unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_http_ssl": 0, } checkRegistryResults(expectedResults, mfs, t) } func TestSucceedIfSelfSignedCA(t *testing.T) { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{InsecureSkipVerify: true}, }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Fail if (not strict) selfsigned CA test fails unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_http_ssl": 1, } checkRegistryResults(expectedResults, mfs, t) } func TestTLSConfigIsIgnoredForPlainHTTP(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{InsecureSkipVerify: false}, }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Fail if InsecureSkipVerify affects simple http fails unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_http_ssl": 0, } checkRegistryResults(expectedResults, mfs, t) } func TestHTTPUsesTargetAsTLSServerName(t *testing.T) { // Create test certificates valid for 1 day. certExpiry := time.Now().AddDate(0, 0, 1) testcertPem, testKeyPem := generateTestCertificate(certExpiry, false) // CAFile must be passed via filesystem, use a tempfile. tmpCaFile, err := ioutil.TempFile("", "cafile.pem") if err != nil { t.Fatalf("Error creating CA tempfile: %s", err) } if _, err = tmpCaFile.Write(testcertPem); err != nil { t.Fatalf("Error writing CA tempfile: %s", err) } if err = tmpCaFile.Close(); err != nil { t.Fatalf("Error closing CA tempfile: %s", err) } defer os.Remove(tmpCaFile.Name()) testcert, err := tls.X509KeyPair(testcertPem, testKeyPem) if err != nil { panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err)) } ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) ts.TLS = &tls.Config{ Certificates: []tls.Certificate{testcert}, } ts.StartTLS() defer ts.Close() registry := prometheus.NewRegistry() module := config.Module{ Timeout: time.Second, HTTP: config.HTTPProbe{ PreferredIPProtocol: "ip4", HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{ CAFile: tmpCaFile.Name(), }, }, }, } // Replace IP address with hostname. url := strings.Replace(ts.URL, "127.0.0.1", "localhost", -1) url = strings.Replace(url, "[::1]", "localhost", -1) result := ProbeHTTP(context.Background(), url, module, registry, log.NewNopLogger()) if !result { t.Fatalf("TLS probe failed unexpectedly") } } func TestHTTPPhases(t *testing.T) { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() // Follow redirect, should succeed with 200. recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{ HTTPClientConfig: pconfig.HTTPClientConfig{ TLSConfig: pconfig.TLSConfig{InsecureSkipVerify: true}, }, }}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("HTTP Phases test failed unexpectedly, got %s", body) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } found := false foundLabel := map[string]bool{ "connect": false, "processing": false, "resolve": false, "transfer": false, "tls": false, } for _, mf := range mfs { if mf.GetName() == "probe_http_duration_seconds" { found = true for _, metric := range mf.GetMetric() { for _, lp := range metric.Label { if lp.GetName() == "phase" { f, ok := foundLabel[lp.GetValue()] if !ok { t.Fatalf("Unexpected label phase=%s", lp.GetValue()) } if f { t.Fatalf("Label phase=%s duplicated", lp.GetValue()) } foundLabel[lp.GetValue()] = true } } } } } if !found { t.Fatal("probe_http_duration_seconds not found") } for lv, found := range foundLabel { if !found { t.Fatalf("Label phase=%s not found", lv) } } } func TestCookieJar(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { expiration := time.Now().Add(365 * 24 * time.Hour) cookie := http.Cookie{Name: "somecookie", Value: "cookie", Expires: expiration} http.SetCookie(w, &cookie) http.Redirect(w, r, "/noredirect", http.StatusFound) } if r.URL.Path == "/noredirect" { cookie, err := r.Cookie("somecookie") if err != nil { t.Fatalf("Error retrieving cookie, got %v", err) } if cookie.String() != "somecookie=cookie" { t.Errorf("Error incorrect cookie value received, got %v, wanted %v", cookie.String(), "somecookie=cookie") } } })) defer ts.Close() recorder := httptest.NewRecorder() registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() result := ProbeHTTP(testCTX, ts.URL, config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{}}, registry, log.NewNopLogger()) body := recorder.Body.String() if !result { t.Fatalf("Redirect test failed unexpectedly, got %s", body) } } prometheus-blackbox-exporter-0.11.0+ds/prober/icmp.go000066400000000000000000000126211321455055500225760ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "bytes" "context" "net" "os" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" "github.com/prometheus/blackbox_exporter/config" ) var ( icmpSequence uint16 icmpSequenceMutex sync.Mutex ) func getICMPSequence() uint16 { icmpSequenceMutex.Lock() defer icmpSequenceMutex.Unlock() icmpSequence++ return icmpSequence } func ProbeICMP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) { var ( socket net.PacketConn requestType icmp.Type replyType icmp.Type ) timeoutDeadline, _ := ctx.Deadline() deadline := time.Now().Add(timeoutDeadline.Sub(time.Now())) ip, _, err := chooseProtocol(module.ICMP.PreferredIPProtocol, target, registry, logger) if err != nil { level.Warn(logger).Log("msg", "Error resolving address", "err", err) return false } level.Info(logger).Log("msg", "Creating socket") if ip.IP.To4() == nil { requestType = ipv6.ICMPTypeEchoRequest replyType = ipv6.ICMPTypeEchoReply socket, err = icmp.ListenPacket("ip6:ipv6-icmp", "::") if err != nil { level.Error(logger).Log("msg", "Error listening to socket", "err", err) return } } else { requestType = ipv4.ICMPTypeEcho replyType = ipv4.ICMPTypeEchoReply if !module.ICMP.DontFragment { socket, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { level.Error(logger).Log("msg", "Error listening to socket", "err", err) return } } else { s, err := net.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { level.Error(logger).Log("msg", "Error listening to socket", "err", err) return } rc, err := ipv4.NewRawConn(s) if err != nil { level.Error(logger).Log("msg", "cannot construct raw connection", "err", err) return } socket = &dfConn{c: rc} } } defer socket.Close() var data []byte if module.ICMP.PayloadSize != 0 { data = make([]byte, module.ICMP.PayloadSize) copy(data, "Prometheus Blackbox Exporter") } else { data = []byte("Prometheus Blackbox Exporter") } body := &icmp.Echo{ ID: os.Getpid() & 0xffff, Seq: int(getICMPSequence()), Data: data, } level.Info(logger).Log("msg", "Creating ICMP packet", "seq", body.Seq, "id", body.ID) wm := icmp.Message{ Type: requestType, Code: 0, Body: body, } wb, err := wm.Marshal(nil) if err != nil { level.Error(logger).Log("msg", "Error marshalling packet", "err", err) return } level.Info(logger).Log("msg", "Writing out packet") if _, err = socket.WriteTo(wb, ip); err != nil { level.Warn(logger).Log("msg", "Error writing to socket", "err", err) return } // Reply should be the same except for the message type. wm.Type = replyType wb, err = wm.Marshal(nil) if err != nil { level.Error(logger).Log("msg", "Error marshalling packet", "err", err) return } rb := make([]byte, 65536) if err := socket.SetReadDeadline(deadline); err != nil { level.Error(logger).Log("msg", "Error setting socket deadline", "err", err) return } level.Info(logger).Log("msg", "Waiting for reply packets") for { n, peer, err := socket.ReadFrom(rb) if err != nil { if nerr, ok := err.(net.Error); ok && nerr.Timeout() { level.Warn(logger).Log("msg", "Timeout reading from socket", "err", err) return } level.Error(logger).Log("msg", "Error reading from socket", "err", err) continue } if peer.String() != ip.String() { continue } if replyType == ipv6.ICMPTypeEchoReply { // Clear checksum to make comparison succeed. rb[2] = 0 rb[3] = 0 } if bytes.Compare(rb[:n], wb) == 0 { level.Info(logger).Log("msg", "Found matching reply packet") return true } } } type dfConn struct { c *ipv4.RawConn } func (c *dfConn) ReadFrom(b []byte) (int, net.Addr, error) { h, p, _, err := c.c.ReadFrom(b) if err != nil { return 0, nil, err } copy(b, p) n := len(b) if len(p) < len(b) { n = len(p) } return n, &net.IPAddr{IP: h.Src}, nil } func (d *dfConn) WriteTo(b []byte, addr net.Addr) (int, error) { ipAddr, err := net.ResolveIPAddr(addr.Network(), addr.String()) if err != nil { return 0, err } dfHeader := &ipv4.Header{ Version: ipv4.Version, Len: ipv4.HeaderLen, Protocol: 1, TotalLen: ipv4.HeaderLen + len(b), Flags: ipv4.DontFragment, TTL: 64, Dst: ipAddr.IP, } return len(b), d.c.WriteTo(dfHeader, b, nil) } func (d *dfConn) Close() error { return d.c.Close() } func (d *dfConn) LocalAddr() net.Addr { return nil } func (d *dfConn) SetDeadline(t time.Time) error { return d.c.SetDeadline(t) } func (d *dfConn) SetReadDeadline(t time.Time) error { return d.c.SetReadDeadline(t) } func (d *dfConn) SetWriteDeadline(t time.Time) error { return d.c.SetWriteDeadline(t) } prometheus-blackbox-exporter-0.11.0+ds/prober/prober.go000066400000000000000000000004551321455055500231410ustar00rootroot00000000000000package prober import ( "context" "github.com/go-kit/kit/log" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/blackbox_exporter/config" ) type ProbeFn func(ctx context.Context, target string, config config.Module, registry *prometheus.Registry, logger log.Logger) bool prometheus-blackbox-exporter-0.11.0+ds/prober/tcp.go000066400000000000000000000145021321455055500224340ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "bufio" "context" "crypto/tls" "fmt" "net" "regexp" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/prometheus/blackbox_exporter/config" ) func dialTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (net.Conn, error) { var dialProtocol, dialTarget string dialer := &net.Dialer{} targetAddress, port, err := net.SplitHostPort(target) if err != nil { level.Error(logger).Log("msg", "Error splitting target address and port", "err", err) return nil, err } ip, _, err := chooseProtocol(module.TCP.PreferredIPProtocol, targetAddress, registry, logger) if err != nil { level.Error(logger).Log("msg", "Error resolving address", "err", err) return nil, err } if ip.IP.To4() == nil { dialProtocol = "tcp6" } else { dialProtocol = "tcp4" } dialTarget = net.JoinHostPort(ip.String(), port) if !module.TCP.TLS { level.Info(logger).Log("msg", "Dialing TCP without TLS") return dialer.DialContext(ctx, dialProtocol, dialTarget) } tlsConfig, err := pconfig.NewTLSConfig(&module.TCP.TLSConfig) if err != nil { level.Error(logger).Log("msg", "Error creating TLS configuration", "err", err) return nil, err } if len(tlsConfig.ServerName) == 0 { // If there is no `server_name` in tls_config, use // targetAddress as TLS-servername. Normally tls.DialWithDialer // would do this for us, but we pre-resolved the name by // `chooseProtocol` and pass the IP-address for dialing (prevents // resolving twice). // For this reason we need to specify the original targetAddress // via tlsConfig to enable hostname verification. tlsConfig.ServerName = targetAddress } timeoutDeadline, _ := ctx.Deadline() dialer.Deadline = timeoutDeadline level.Info(logger).Log("msg", "Dialing TCP with TLS") return tls.DialWithDialer(dialer, dialProtocol, dialTarget, tlsConfig) } func ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) bool { probeSSLEarliestCertExpiry := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_ssl_earliest_cert_expiry", Help: "Returns earliest SSL cert expiry date", }) probeFailedDueToRegex := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_failed_due_to_regex", Help: "Indicates if probe failed due to regex", }) registry.MustRegister(probeFailedDueToRegex) deadline, _ := ctx.Deadline() conn, err := dialTCP(ctx, target, module, registry, logger) if err != nil { level.Error(logger).Log("msg", "Error dialing TCP", "err", err) return false } defer conn.Close() level.Info(logger).Log("msg", "Successfully dialed") // Set a deadline to prevent the following code from blocking forever. // If a deadline cannot be set, better fail the probe by returning an error // now rather than blocking forever. if err := conn.SetDeadline(deadline); err != nil { level.Error(logger).Log("msg", "Error setting deadline", "err", err) return false } if module.TCP.TLS { state := conn.(*tls.Conn).ConnectionState() registry.MustRegister(probeSSLEarliestCertExpiry) probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix())) } scanner := bufio.NewScanner(conn) for i, qr := range module.TCP.QueryResponse { level.Info(logger).Log("msg", "Processing query response entry", "entry_number", i) send := qr.Send if qr.Expect != "" { re, err := regexp.Compile(qr.Expect) if err != nil { level.Error(logger).Log("msg", "Could not compile into regular expression", "regexp", qr.Expect, "err", err) return false } var match []int // Read lines until one of them matches the configured regexp. for scanner.Scan() { level.Debug(logger).Log("msg", "Read line", "line", scanner.Text()) match = re.FindSubmatchIndex(scanner.Bytes()) if match != nil { level.Info(logger).Log("msg", "Regexp matched", "regexp", re, "line", scanner.Text()) break } } if scanner.Err() != nil { level.Error(logger).Log("msg", "Error reading from connection", "err", scanner.Err().Error()) return false } if match == nil { probeFailedDueToRegex.Set(1) level.Error(logger).Log("msg", "Regexp did not match", "regexp", re, "line", scanner.Text()) return false } probeFailedDueToRegex.Set(0) send = string(re.Expand(nil, []byte(send), scanner.Bytes(), match)) } if send != "" { level.Debug(logger).Log("msg", "Sending line", "line", send) if _, err := fmt.Fprintf(conn, "%s\n", send); err != nil { level.Error(logger).Log("msg", "Failed to send", "err", err) return false } } if qr.StartTLS { // Upgrade TCP connection to TLS. tlsConfig, err := pconfig.NewTLSConfig(&module.TCP.TLSConfig) if err != nil { level.Error(logger).Log("msg", "Failed to create TLS configuration", "err", err) return false } if tlsConfig.ServerName == "" { // Use target-hostname as default for TLS-servername. targetAddress, _, _ := net.SplitHostPort(target) // Had succeeded in dialTCP already. tlsConfig.ServerName = targetAddress } tlsConn := tls.Client(conn, tlsConfig) defer tlsConn.Close() // Initiate TLS handshake (required here to get TLS state). if err := tlsConn.Handshake(); err != nil { level.Error(logger).Log("msg", "TLS Handshake (client) failed", "err", err) return false } level.Info(logger).Log("msg", "TLS Handshake (client) succeeded.") conn = net.Conn(tlsConn) scanner = bufio.NewScanner(conn) // Get certificate expiry. state := tlsConn.ConnectionState() registry.MustRegister(probeSSLEarliestCertExpiry) probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix())) } } return true } prometheus-blackbox-exporter-0.11.0+ds/prober/tcp_test.go000066400000000000000000000401561321455055500234770ustar00rootroot00000000000000// Copyright 2015 The Prometheus Authors // Licensed 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. package prober import ( "context" "crypto/tls" "fmt" "io/ioutil" "net" "os" "runtime" "testing" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/prometheus/blackbox_exporter/config" ) func TestTCPConnection(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() ch := make(chan (struct{})) go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } conn.Close() ch <- struct{}{} }() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() registry := prometheus.NewRegistry() if !ProbeTCP(testCTX, ln.Addr().String(), config.Module{}, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } <-ch } func TestTCPConnectionFails(t *testing.T) { // Invalid port number. registry := prometheus.NewRegistry() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if ProbeTCP(testCTX, ":0", config.Module{}, registry, log.NewNopLogger()) { t.Fatalf("TCP module suceeded, expected failure.") } } func TestTCPConnectionWithTLS(t *testing.T) { ln, err := net.Listen("tcp", ":0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() _, listenPort, _ := net.SplitHostPort(ln.Addr().String()) testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Create test certificates valid for 1 day. certExpiry := time.Now().AddDate(0, 0, 1) testcert_pem, testkey_pem := generateTestCertificate(certExpiry, false) // CAFile must be passed via filesystem, use a tempfile. tmpCaFile, err := ioutil.TempFile("", "cafile.pem") if err != nil { t.Fatalf(fmt.Sprintf("Error creating CA tempfile: %s", err)) } if _, err := tmpCaFile.Write(testcert_pem); err != nil { t.Fatalf(fmt.Sprintf("Error writing CA tempfile: %s", err)) } if err := tmpCaFile.Close(); err != nil { t.Fatalf(fmt.Sprintf("Error closing CA tempfile: %s", err)) } defer os.Remove(tmpCaFile.Name()) ch := make(chan (struct{})) logger := log.NewNopLogger() // Handle server side of this test. serverFunc := func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } defer conn.Close() testcert, err := tls.X509KeyPair(testcert_pem, testkey_pem) if err != nil { panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err)) } // Immediately upgrade to TLS. tlsConfig := &tls.Config{ ServerName: "localhost", Certificates: []tls.Certificate{testcert}, } tlsConn := tls.Server(conn, tlsConfig) defer tlsConn.Close() if err := tlsConn.Handshake(); err != nil { level.Error(logger).Log("msg", "Error TLS Handshake (server) failed", "err", err) } else { // Send some bytes before terminating the connection. fmt.Fprintf(tlsConn, "Hello World!\n") } ch <- struct{}{} } // Expect name-verified TLS connection. module := config.Module{ TCP: config.TCPProbe{ TLS: true, TLSConfig: pconfig.TLSConfig{ CAFile: tmpCaFile.Name(), InsecureSkipVerify: false, }, }, } registry := prometheus.NewRegistry() go serverFunc() // Test name-verification failure (IP without IPs in cert's SAN). if ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module succeeded, expected failure.") } <-ch registry = prometheus.NewRegistry() go serverFunc() // Test name-verification with name from target. target := net.JoinHostPort("localhost", listenPort) if !ProbeTCP(testCTX, target, module, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } <-ch registry = prometheus.NewRegistry() go serverFunc() // Test name-verification against name from tls_config. module.TCP.TLSConfig.ServerName = "localhost" if !ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } <-ch // Check the probe_ssl_earliest_cert_expiry. mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_ssl_earliest_cert_expiry": float64(certExpiry.Unix()), } checkRegistryResults(expectedResults, mfs, t) } func TestTCPConnectionQueryResponseStartTLS(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Create test certificates valid for 1 day. certExpiry := time.Now().AddDate(0, 0, 1) testcert_pem, testkey_pem := generateTestCertificate(certExpiry, true) // CAFile must be passed via filesystem, use a tempfile. tmpCaFile, err := ioutil.TempFile("", "cafile.pem") if err != nil { t.Fatalf(fmt.Sprintf("Error creating CA tempfile: %s", err)) } if _, err := tmpCaFile.Write(testcert_pem); err != nil { t.Fatalf(fmt.Sprintf("Error writing CA tempfile: %s", err)) } if err := tmpCaFile.Close(); err != nil { t.Fatalf(fmt.Sprintf("Error closing CA tempfile: %s", err)) } defer os.Remove(tmpCaFile.Name()) // Define some (bogus) example SMTP dialog with STARTTLS. module := config.Module{ TCP: config.TCPProbe{ QueryResponse: []config.QueryResponse{ {Expect: "^220.*ESMTP.*$"}, {Send: "EHLO tls.prober"}, {Expect: "^250-STARTTLS"}, {Send: "STARTTLS"}, {Expect: "^220"}, {StartTLS: true}, {Send: "EHLO tls.prober"}, {Expect: "^250-AUTH"}, {Send: "QUIT"}, }, TLSConfig: pconfig.TLSConfig{ CAFile: tmpCaFile.Name(), InsecureSkipVerify: false, }, }, } // Handle server side of this test. ch := make(chan (struct{})) go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } defer conn.Close() fmt.Fprintf(conn, "220 ESMTP StartTLS pseudo-server\n") if _, e := fmt.Fscanf(conn, "EHLO tls.prober\n"); e != nil { panic("Error in dialog. No EHLO received.") } fmt.Fprintf(conn, "250-pseudo-server.example.net\n") fmt.Fprintf(conn, "250-STARTTLS\n") fmt.Fprintf(conn, "250 DSN\n") if _, e := fmt.Fscanf(conn, "STARTTLS\n"); e != nil { panic("Error in dialog. No (TLS) STARTTLS received.") } fmt.Fprintf(conn, "220 2.0.0 Ready to start TLS\n") testcert, err := tls.X509KeyPair(testcert_pem, testkey_pem) if err != nil { panic(fmt.Sprintf("Failed to decode TLS testing keypair: %s\n", err)) } // Do the server-side upgrade to TLS. tlsConfig := &tls.Config{ ServerName: "localhost", Certificates: []tls.Certificate{testcert}, } tlsConn := tls.Server(conn, tlsConfig) if err := tlsConn.Handshake(); err != nil { panic(fmt.Sprintf("TLS Handshake (server) failed: %s\n", err)) } defer tlsConn.Close() // Continue encrypted. if _, e := fmt.Fscanf(tlsConn, "EHLO"); e != nil { panic("Error in dialog. No (TLS) EHLO received.") } fmt.Fprintf(tlsConn, "250-AUTH\n") fmt.Fprintf(tlsConn, "250 DSN\n") ch <- struct{}{} }() // Do the client side of this test. registry := prometheus.NewRegistry() if !ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } <-ch // Check the probe_ssl_earliest_cert_expiry. mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_ssl_earliest_cert_expiry": float64(certExpiry.Unix()), } checkRegistryResults(expectedResults, mfs, t) } func TestTCPConnectionQueryResponseIRC(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() module := config.Module{ TCP: config.TCPProbe{ QueryResponse: []config.QueryResponse{ {Send: "NICK prober"}, {Send: "USER prober prober prober :prober"}, {Expect: "^:[^ ]+ 001"}, }, }, } ch := make(chan (struct{})) go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } fmt.Fprintf(conn, ":ircd.localhost NOTICE AUTH :*** Looking up your hostname...\n") var nick, user, mode, unused, realname string fmt.Fscanf(conn, "NICK %s", &nick) fmt.Fscanf(conn, "USER %s %s %s :%s", &user, &mode, &unused, &realname) fmt.Fprintf(conn, ":ircd.localhost 001 %s :Welcome to IRC!\n", nick) conn.Close() ch <- struct{}{} }() registry := prometheus.NewRegistry() if !ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } <-ch go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } fmt.Fprintf(conn, ":ircd.localhost NOTICE AUTH :*** Looking up your hostname...\n") var nick, user, mode, unused, realname string fmt.Fscanf(conn, "NICK %s", &nick) fmt.Fscanf(conn, "USER %s %s %s :%s", &user, &mode, &unused, &realname) fmt.Fprintf(conn, "ERROR: Your IP address has been blacklisted.\n") conn.Close() ch <- struct{}{} }() registry = prometheus.NewRegistry() if ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module succeeded, expected failure.") } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_failed_due_to_regex": 1, } checkRegistryResults(expectedResults, mfs, t) <-ch } func TestTCPConnectionQueryResponseMatching(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() time.Sleep(time.Millisecond * 100) module := config.Module{ TCP: config.TCPProbe{ QueryResponse: []config.QueryResponse{ { Expect: "SSH-2.0-(OpenSSH_6.9p1) Debian-2", Send: "CONFIRM ${1}", }, }, }, } ch := make(chan string) go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } conn.SetDeadline(time.Now().Add(1 * time.Second)) fmt.Fprintf(conn, "SSH-2.0-OpenSSH_6.9p1 Debian-2\n") var version string fmt.Fscanf(conn, "CONFIRM %s", &version) conn.Close() ch <- version }() registry := prometheus.NewRegistry() if !ProbeTCP(testCTX, ln.Addr().String(), module, registry, log.NewNopLogger()) { t.Fatalf("TCP module failed, expected success.") } if got, want := <-ch, "OpenSSH_6.9p1"; got != want { t.Fatalf("Read unexpected version: got %q, want %q", got, want) } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_failed_due_to_regex": 0, } checkRegistryResults(expectedResults, mfs, t) } func TestTCPConnectionProtocol(t *testing.T) { // This test assumes that listening TCP listens both IPv6 and IPv4 traffic and // localhost resolves to both 127.0.0.1 and ::1. we must skip the test if either // of these isn't true. This should be true for modern Linux systems. if runtime.GOOS == "dragonfly" || runtime.GOOS == "openbsd" { t.Skip("IPv6 socket isn't able to accept IPv4 traffic in the system.") } _, err := net.ResolveIPAddr("ip6", "localhost") if err != nil { t.Skip("\"localhost\" doesn't resolve to ::1.") } ln, err := net.Listen("tcp", ":0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, port, _ := net.SplitHostPort(ln.Addr().String()) // Force IPv4 module := config.Module{ TCP: config.TCPProbe{ PreferredIPProtocol: "ip4", }, } registry := prometheus.NewRegistry() result := ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP protocol: \"tcp4\" connection test failed, expected success.") } mfs, err := registry.Gather() if err != nil { t.Fatal(err) } expectedResults := map[string]float64{ "probe_ip_protocol": 4, } checkRegistryResults(expectedResults, mfs, t) // Force IPv6 module = config.Module{ TCP: config.TCPProbe{}, } registry = prometheus.NewRegistry() result = ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP protocol: \"tcp6\" connection test failed, expected success.") } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // Prefer IPv4 module = config.Module{ TCP: config.TCPProbe{ PreferredIPProtocol: "ip4", }, } registry = prometheus.NewRegistry() result = ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP protocol: \"tcp\", prefer: \"ip4\" connection test failed, expected success.") } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 4, } checkRegistryResults(expectedResults, mfs, t) // Prefer IPv6 module = config.Module{ TCP: config.TCPProbe{ PreferredIPProtocol: "ip6", }, } registry = prometheus.NewRegistry() result = ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP protocol: \"tcp\", prefer: \"ip6\" connection test failed, expected success.") } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // Prefer nothing module = config.Module{ TCP: config.TCPProbe{}, } registry = prometheus.NewRegistry() result = ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP protocol: \"tcp\" connection test failed, expected success.") } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) // No protocol module = config.Module{ TCP: config.TCPProbe{}, } registry = prometheus.NewRegistry() result = ProbeTCP(testCTX, net.JoinHostPort("localhost", port), module, registry, log.NewNopLogger()) if !result { t.Fatalf("TCP connection test with protocol unspecified failed, expected success.") } mfs, err = registry.Gather() if err != nil { t.Fatal(err) } expectedResults = map[string]float64{ "probe_ip_protocol": 6, } checkRegistryResults(expectedResults, mfs, t) } func TestPrometheusTimeoutTCP(t *testing.T) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("Error listening on socket: %s", err) } defer ln.Close() ch := make(chan (struct{})) go func() { conn, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting on socket: %s", err)) } conn.Close() ch <- struct{}{} }() testCTX, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() registry := prometheus.NewRegistry() if ProbeTCP(testCTX, ln.Addr().String(), config.Module{TCP: config.TCPProbe{ QueryResponse: []config.QueryResponse{ { Expect: "SSH-2.0-(OpenSSH_6.9p1) Debian-2", }, }, }}, registry, log.NewNopLogger()) { t.Fatalf("TCP module succeeded, expected timeout failure.") } <-ch } prometheus-blackbox-exporter-0.11.0+ds/prober/tls.go000066400000000000000000000016341321455055500224520ustar00rootroot00000000000000// Copyright 2016 The Prometheus Authors // Licensed 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. package prober import ( "crypto/tls" "time" ) func getEarliestCertExpiry(state *tls.ConnectionState) time.Time { earliest := time.Time{} for _, cert := range state.PeerCertificates { if (earliest.IsZero() || cert.NotAfter.Before(earliest)) && !cert.NotAfter.IsZero() { earliest = cert.NotAfter } } return earliest } prometheus-blackbox-exporter-0.11.0+ds/prober/utils.go000066400000000000000000000036211321455055500230060ustar00rootroot00000000000000package prober import ( "net" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus/client_golang/prometheus" ) // Returns the IP for the preferedIPProtocol and lookup time. func chooseProtocol(preferredIPProtocol string, target string, registry *prometheus.Registry, logger log.Logger) (ip *net.IPAddr, lookupTime float64, err error) { var fallbackProtocol string probeDNSLookupTimeSeconds := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_dns_lookup_time_seconds", Help: "Returns the time taken for probe dns lookup in seconds", }) probeIPProtocolGauge := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "probe_ip_protocol", Help: "Specifies whether probe ip protocol is IP4 or IP6", }) registry.MustRegister(probeIPProtocolGauge) registry.MustRegister(probeDNSLookupTimeSeconds) if preferredIPProtocol == "ip6" || preferredIPProtocol == "" { preferredIPProtocol = "ip6" fallbackProtocol = "ip4" } else { preferredIPProtocol = "ip4" fallbackProtocol = "ip6" } if preferredIPProtocol == "ip6" { fallbackProtocol = "ip4" } else { fallbackProtocol = "ip6" } level.Info(logger).Log("msg", "Resolving target address", "preferred_ip_protocol", preferredIPProtocol) resolveStart := time.Now() defer func() { lookupTime = time.Since(resolveStart).Seconds() probeDNSLookupTimeSeconds.Add(lookupTime) }() ip, err = net.ResolveIPAddr(preferredIPProtocol, target) if err != nil { level.Warn(logger).Log("msg", "Resolution with preferred IP protocol failed, attempting fallback protocol", "fallback_protocol", fallbackProtocol, "err", err) ip, err = net.ResolveIPAddr(fallbackProtocol, target) if err != nil { return ip, 0.0, err } } if ip.IP.To4() == nil { probeIPProtocolGauge.Set(6) } else { probeIPProtocolGauge.Set(4) } level.Info(logger).Log("msg", "Resolved target address", "ip", ip) return ip, lookupTime, nil } prometheus-blackbox-exporter-0.11.0+ds/prober/utils_test.go000066400000000000000000000041461321455055500240500ustar00rootroot00000000000000package prober import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "testing" "time" dto "github.com/prometheus/client_model/go" ) // Check if expected results are in the registry func checkRegistryResults(expRes map[string]float64, mfs []*dto.MetricFamily, t *testing.T) { res := make(map[string]float64) for i := range mfs { res[mfs[i].GetName()] = mfs[i].Metric[0].GetGauge().GetValue() } for k, v := range expRes { val, ok := res[k] if !ok { t.Fatalf("Expected metric %v not found in returned metrics", k) } if val != v { t.Fatalf("Expected: %v: %v, got: %v: %v", k, v, k, val) } } } // Create test certificate with specified expiry date // Certificate will be self-signed and use localhost/127.0.0.1 // Generated certificate and key are returned in PEM encoding func generateTestCertificate(expiry time.Time, IPAddressSAN bool) ([]byte, []byte) { privatekey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(fmt.Sprintf("Error creating rsa key: %s", err)) } publickey := &privatekey.PublicKey cert := x509.Certificate{ IsCA: true, BasicConstraintsValid: true, SubjectKeyId: []byte{1}, SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Example Org"}, }, NotBefore: time.Now(), NotAfter: expiry, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, } cert.DNSNames = append(cert.DNSNames, "localhost") if IPAddressSAN { cert.IPAddresses = append(cert.IPAddresses, net.ParseIP("127.0.0.1")) cert.IPAddresses = append(cert.IPAddresses, net.ParseIP("::1")) } derCert, err := x509.CreateCertificate(rand.Reader, &cert, &cert, publickey, privatekey) if err != nil { panic(fmt.Sprintf("Error signing test-certificate: %s", err)) } pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derCert}) pemKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privatekey)}) return pemCert, pemKey } prometheus-blackbox-exporter-0.11.0+ds/vendor/000077500000000000000000000000001321455055500213215ustar00rootroot00000000000000prometheus-blackbox-exporter-0.11.0+ds/vendor/vendor.json000066400000000000000000000240261321455055500235150ustar00rootroot00000000000000{ "comment": "", "ignore": "test", "package": [ { "checksumSHA1": "KmjnydoAbofMieIWm+it5OWERaM=", "path": "github.com/alecthomas/template", "revision": "a0175ee3bccc567396460bf5acd36800cb10c49c", "revisionTime": "2016-04-05T07:15:01Z" }, { "checksumSHA1": "3wt0pTXXeS+S93unwhGoLIyGX/Q=", "path": "github.com/alecthomas/template/parse", "revision": "a0175ee3bccc567396460bf5acd36800cb10c49c", "revisionTime": "2016-04-05T07:15:01Z" }, { "checksumSHA1": "fCc3grA7vIxfBru7R3SqjcW+oLI=", "path": "github.com/alecthomas/units", "revision": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a", "revisionTime": "2015-10-22T06:55:26Z" }, { "checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=", "path": "github.com/beorn7/perks/quantile", "revision": "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9", "revisionTime": "2016-08-04T10:47:26Z" }, { "checksumSHA1": "KrIRJ4p3nRze4NcxfgX5+N9+D1M=", "path": "github.com/go-kit/kit/log", "revision": "0d313fb5fb3a94d87d61e6434785264e87a5d740", "revisionTime": "2017-09-17T20:27:34Z" }, { "checksumSHA1": "t7aTpDH0h4BZcGU0KkUr14QQG2w=", "path": "github.com/go-kit/kit/log/level", "revision": "0d313fb5fb3a94d87d61e6434785264e87a5d740", "revisionTime": "2017-09-17T20:27:34Z" }, { "checksumSHA1": "KxX/Drph+byPXBFIXaCZaCOAnrU=", "path": "github.com/go-logfmt/logfmt", "revision": "390ab7935ee28ec6b286364bba9b4dd6410cb3d5", "revisionTime": "2016-11-15T14:25:13Z" }, { "checksumSHA1": "j6vhe49MX+dyHR9rU91P6vMx55o=", "path": "github.com/go-stack/stack", "revision": "817915b46b97fd7bb80e8ab6b69f01a53ac3eebf", "revisionTime": "2017-07-24T01:23:01Z" }, { "checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=", "path": "github.com/golang/protobuf/proto", "revision": "ae59567b9aab61b50b2590679a62c3c044030b61", "revisionTime": "2017-09-19T00:21:09Z" }, { "checksumSHA1": "abKzFXAn0KDr5U+JON1ZgJ2lUtU=", "path": "github.com/kr/logfmt", "revision": "b84e30acd515aadc4b783ad4ff83aff3299bdfe0", "revisionTime": "2014-02-26T03:06:59Z" }, { "checksumSHA1": "bKMZjd2wPw13VwoE7mBeSv5djFA=", "path": "github.com/matttproud/golang_protobuf_extensions/pbutil", "revision": "c12348ce28de40eed0136aa2b644d0ee0650e56c", "revisionTime": "2016-04-24T11:30:07Z" }, { "checksumSHA1": "Ewgc6lPiuDZVOf1407rxzSgAOag=", "path": "github.com/miekg/dns", "revision": "e4205768578dc90c2669e75a2f8a8bf77e3083a4", "revisionTime": "2017-08-18T13:14:42Z" }, { "checksumSHA1": "rJab1YdNhQooDiBWNnt7TLWPyBU=", "path": "github.com/pkg/errors", "revision": "2b3a18b5f0fb6b4f9190549597d3f962c02bc5eb", "revisionTime": "2017-09-10T13:46:14Z" }, { "checksumSHA1": "ty3Y0hPtRphsqcykY9ihV6F02Fk=", "path": "github.com/prometheus/client_golang/prometheus", "revision": "50b3332fd63be43e38472600ec187ceec39d26d6", "revisionTime": "2017-09-13T10:48:29Z" }, { "checksumSHA1": "wsAkYlRRUNx+OAuUOIqdjO7dICM=", "path": "github.com/prometheus/client_golang/prometheus/promhttp", "revision": "50b3332fd63be43e38472600ec187ceec39d26d6", "revisionTime": "2017-09-13T10:48:29Z" }, { "checksumSHA1": "DvwvOlPNAgRntBzt3b3OSRMS2N4=", "path": "github.com/prometheus/client_model/go", "revision": "6f3806018612930941127f2a7c6c453ba2c527d2", "revisionTime": "2017-02-16T18:52:47Z" }, { "checksumSHA1": "4TLgSCgJZuS5gtytxNvcVk4h8/g=", "path": "github.com/prometheus/common/config", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "xfnn0THnqNwjwimeTClsxahYrIo=", "path": "github.com/prometheus/common/expfmt", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "GWlM3d2vPYyNATtTFgftS10/A9w=", "path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "3VoqH7TFfzA6Ds0zFzIbKCUvBmw=", "path": "github.com/prometheus/common/model", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "Yseprf8kAFr/s7wztkQnrFuFN+8=", "path": "github.com/prometheus/common/promlog", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "1H28FCxsaAIm6kvue+Wfdd8Lq6M=", "path": "github.com/prometheus/common/promlog/flag", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "91KYK0SpvkaMJJA2+BcxbVnyRO0=", "path": "github.com/prometheus/common/version", "revision": "2f17f4a9d485bf34b4bfaccc273805040e4f86c8", "revisionTime": "2017-09-08T16:18:22Z" }, { "checksumSHA1": "ihxJIjxtbEYdQKwA0D0nRipj95I=", "path": "github.com/prometheus/procfs", "revision": "e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2", "revisionTime": "2017-07-03T10:12:42Z" }, { "checksumSHA1": "xCiFAAwVTrjsfZT1BIJQ3DgeNCY=", "path": "github.com/prometheus/procfs/xfs", "revision": "e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2", "revisionTime": "2017-07-03T10:12:42Z" }, { "checksumSHA1": "uX2McdP4VcQ6zkAF0Q4oyd0rFtU=", "path": "golang.org/x/net/bpf", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "DqdFGWbLLyVFeDvzvXyf1Y678uA=", "path": "golang.org/x/net/icmp", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "RcrB7tgYS/GMW4QrwVdMOTNqIU8=", "path": "golang.org/x/net/idna", "revision": "01c190206fbdffa42f334f4b2bf2220f50e64920", "revisionTime": "2017-11-02T18:53:09Z" }, { "checksumSHA1": "YoSf+PgTWvHmFVaF3MrtZz3kX38=", "path": "golang.org/x/net/internal/iana", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "5eWwtQVHJ0Lp2/538Kve2KuE8gQ=", "path": "golang.org/x/net/internal/socket", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "ZGMENpNTj2hojdJMcrUO+UPKVgE=", "path": "golang.org/x/net/ipv4", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "QUvByKIVmIy9c+8+O1XGyh9ynoY=", "path": "golang.org/x/net/ipv6", "revision": "8351a756f30f1297fe94bbf4b767ec589c6ea6d0", "revisionTime": "2017-09-15T01:39:56Z" }, { "checksumSHA1": "nt4o3cMOyP9prgVeAZgRf82/Et0=", "path": "golang.org/x/net/publicsuffix", "revision": "01c190206fbdffa42f334f4b2bf2220f50e64920", "revisionTime": "2017-11-02T18:53:09Z" }, { "checksumSHA1": "5AKasas4CO8g/Wiizgd34EGLoMQ=", "path": "golang.org/x/text/collate", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "JGxWXCzR7rwOsuQCK1UGd6yuS90=", "path": "golang.org/x/text/collate/build", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "45fiqnr0oU2bicWWAWz0lGWg4eU=", "path": "golang.org/x/text/internal/colltab", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "ZQdHbB9VYCXwQ+9/CmZPhJv0+SM=", "path": "golang.org/x/text/internal/gen", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "hyNCcTwMQnV6/MK8uUW9E5H0J0M=", "path": "golang.org/x/text/internal/tag", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "47nwiUyVBY2RKoEGXmCSvusY4Js=", "path": "golang.org/x/text/internal/triegen", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "brtRuRoLzfZwY4Bir6gjFZqzSME=", "path": "golang.org/x/text/internal/ucd", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "YsHNCKLl/81IAeBJUjHE4uqAPLM=", "path": "golang.org/x/text/language", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "tltivJ/uj/lqLk05IqGfCv2F/E8=", "path": "golang.org/x/text/secure/bidirule", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=", "path": "golang.org/x/text/transform", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "tk+lpF2CDV7e5RwwRY5ZTCGrd9o=", "path": "golang.org/x/text/unicode/bidi", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "+Tf0E0XN8mhHx24ustbgB0sfeXM=", "path": "golang.org/x/text/unicode/cldr", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "BwRNKgzIMUxk56OScxyr43BV6IE=", "path": "golang.org/x/text/unicode/norm", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "lpXLU5EzpB421tEcjJ1m6fIL7eQ=", "path": "golang.org/x/text/unicode/rangetable", "revision": "88f656faf3f37f690df1a32515b479415e1a6769", "revisionTime": "2017-10-26T07:52:28Z" }, { "checksumSHA1": "3SZTatHIy9OTKc95YlVfXKnoySg=", "path": "gopkg.in/alecthomas/kingpin.v2", "revision": "1087e65c9441605df944fb12c33f0fe7072d18ca", "revisionTime": "2017-07-27T04:22:29Z" }, { "checksumSHA1": "RDJpJQwkF012L6m/2BJizyOksNw=", "path": "gopkg.in/yaml.v2", "revision": "eb3733d160e74a9c7e442f435eb3bea458e1d19f", "revisionTime": "2017-08-12T16:00:11Z" } ], "rootPath": "github.com/prometheus/blackbox_exporter" }