diff --git a/README.md b/README.md index 3165209..ca1f390 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,16 @@ meaningful visualisations and consoles. - [Usage](#usage) - [Metrics](#metrics) - [Configuration](#configuration) - - [Configuration file](#configuration-file) - - [<module>](#module) - - [<tls_config>](#tls_config) - - [<https_probe>](#https_probe) - - [<tcp_probe>](#tcp_probe) + - [TCP](#tcp) + - [HTTPS](#https) + - [File](#file) + - [Configuration file](#configuration-file) + - [<module>](#module) + - [<tls_config>](#tls_config) + - [<https_probe>](#https_probe) + - [<tcp_probe>](#tcp_probe) - [Example Queries](#example-queries) - - [Peer Cerificates vs Verified Chain Certificates](#peer-cerificates-vs-verified-chain-certificates) - - [Proxying](#proxying) + - [Peer Certificates vs Verified Chain Certificates](#peer-certificates-vs-verified-chain-certificates) - [Grafana](#grafana) Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) @@ -47,7 +49,7 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) Similarly to the blackbox_exporter, visiting [http://localhost:9219/probe?target=example.com:443](http://localhost:9219/probe?target=example.com:443) -will return certificate metrics for example.com. The `ssl_tls_connect_success` +will return certificate metrics for example.com. The `ssl_probe_success` metric indicates if the probe has been successful. ### Docker @@ -88,24 +90,28 @@ Flags: ## Metrics -| Metric | Meaning | Labels | -| ----------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | -| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | -| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | -| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | | -| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | | -| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | | -| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | | -| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | -| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober | -| ssl_tls_connect_success | Was the TLS connection successful? Boolean. | | -| ssl_tls_version_info | The TLS version used. Always 1. | version | -| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | -| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| Metric | Meaning | Labels | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| ssl_file_cert_not_after | The date after which a certificate found by the file prober expires. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| ssl_file_cert_not_before | The date before which a certificate found by the file prober is not valid. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | +| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | | +| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | | +| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | | +| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | | +| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | +| ssl_probe_success | Was the probe successful? Boolean. | | +| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober | +| ssl_tls_version_info | The TLS version used. Always 1. | version | +| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | +| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | ## Configuration +### TCP + Just like with the blackbox_exporter, you should pass the targets to a single instance of the exporter in a scrape config with a clever bit of relabelling. This allows you to leverage service discovery and keeps configuration @@ -128,8 +134,11 @@ scrape_configs: replacement: 127.0.0.1:9219 # SSL exporter. ``` -By default the exporter will make a TCP connection to the target. You can change -this to https by setting the module parameter: +### HTTPS + +By default the exporter will make a TCP connection to the target. This will be +suitable for most cases but if you want to take advantage of http proxying you +can use a HTTPS client by setting the `https` module parameter: ```yml scrape_configs: @@ -150,7 +159,53 @@ scrape_configs: replacement: 127.0.0.1:9219 ``` -### Configuration file +This will use proxy servers discovered by the environment variables `HTTP_PROXY`, +`HTTPS_PROXY` and `ALL_PROXY`. Or, you can set the `proxy_url` option in the module +configuration. + +The latter takes precedence. + +### File + +The `file` prober exports `ssl_file_cert_not_after` and +`ssl_file_cert_not_before` for PEM encoded certificates found in local files. + +Files local to the exporter can be scraped by providing them as the target +parameter: + +``` +curl "localhost:9219/probe?module=file&target=/etc/ssl/cert.pem" +``` + +The target parameter supports globbing (as provided by the +[doublestar](https://github.com/bmatcuk/doublestar) package), +which allows you to capture multiple files at once: + +``` +curl "localhost:9219/probe?module=file&target=/etc/ssl/**/*.pem" +``` + +One specific usage of this prober could be to run the exporter as a DaemonSet in +Kubernetes and then scrape each instance to check the expiry of certificates on +each node: + +``` +scrape_configs: + - job_name: "ssl" + metrics_path: /probe + params: + module: ["file"] + target: ["/etc/kubernetes/**/*.crt"] + kubernetes_sd_configs: + - role: node + relabel_configs: + - source_labels: [__address__] + regex: ^(.*):(.*)$ + target_label: __address__ + replacement: ${1}:9219 +``` + +## Configuration file You can provide further module configuration by providing the path to a configuration file with `--config.file`. The file is written in yaml format, @@ -160,10 +215,10 @@ defined by the schema below. modules: [] ``` -#### \ +### \ ``` -# The protocol over which the probe will take place (https, tcp) +# The type of probe (https, tcp, file) prober: # How long the probe will wait before giving up. @@ -177,7 +232,7 @@ prober: [ tcp: ] ``` -#### +### ``` # Disable target certificate validation. @@ -196,14 +251,14 @@ prober: [ server_name: ] ``` -#### +### ``` # HTTP proxy server to use to connect to the targets. [ proxy_url: ] ``` -#### +### ``` # Use the STARTTLS command before starting TLS for those protocols that support it (smtp, ftp, imap) @@ -237,13 +292,13 @@ Number of certificates presented by the server: count(ssl_cert_not_after) by (instance) ``` -Identify instances that have failed to create a valid SSL connection: +Identify failed probes: ``` -ssl_tls_connect_success == 0 +ssl_probe_success == 0 ``` -## Peer Cerificates vs Verified Chain Certificates +## Peer Certificates vs Verified Chain Certificates Metrics are exported for the `NotAfter` and `NotBefore` fields for peer certificates as well as for the verified chain that is @@ -277,20 +332,6 @@ of trust between the exporter and the target. Genuine clients may hold different root certs than the exporter and therefore have different verified chains of trust. -## Proxying - -The `https` prober supports the use of proxy servers discovered by the -environment variables `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY`. - -For instance: - - $ export HTTPS_PROXY=localhost:8888 - $ ./ssl_exporter - -Or, you can set the `proxy_url` option in the module. - -The latter takes precedence. - ## Grafana You can find a simple dashboard [here](grafana/dashboard.json) that tracks diff --git a/config/config.go b/config/config.go index debf82f..4e05437 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,9 @@ var ( "https": Module{ Prober: "https", }, + "file": Module{ + Prober: "file", + }, }, } ) @@ -42,7 +45,6 @@ func LoadConfig(confFile string) (*Config, error) { } return c, nil - } type Config struct { diff --git a/examples/example.prometheus.yml b/examples/example.prometheus.yml index 82addb6..ac6c1e9 100644 --- a/examples/example.prometheus.yml +++ b/examples/example.prometheus.yml @@ -17,4 +17,12 @@ scrape_configs: - source_labels: [__param_target] target_label: instance - target_label: __address__ - replacement: 127.0.0.1:9219 # SSL exporter. \ No newline at end of file + replacement: 127.0.0.1:9219 # SSL exporter. + - job_name: 'ssl-files' + metrics_path: /probe + params: + module: ["file"] + target: ["/etc/ssl/**/*.pem"] + static_configs: + - targets: + - 127.0.0.1:9219 diff --git a/go.mod b/go.mod index b16edcd..e56851c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,9 @@ module github.com/ribbybibby/ssl_exporter require ( + github.com/bmatcuk/doublestar/v2 v2.0.3 github.com/prometheus/client_golang v1.8.0 + github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.14.0 github.com/sirupsen/logrus v1.7.0 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 diff --git a/go.sum b/go.sum index f62517b..004a319 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v2 v2.0.3 h1:D6SI8MzWzXXBXZFS87cFL6s/n307lEU+thM2SUnge3g= +github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/grafana/dashboard.json b/grafana/dashboard.json index 84f5fb2..09ff0c1 100644 --- a/grafana/dashboard.json +++ b/grafana/dashboard.json @@ -240,7 +240,7 @@ "steppedLine": false, "targets": [ { - "expr": "(up{job=~\"$job\", instance=~\"$instance\"} == 0 or ssl_tls_connect_success{job=~\"$job\", instance=~\"$instance\"} == 0)^0", + "expr": "(up{job=~\"$job\", instance=~\"$instance\"} == 0 or ssl_probe_success{job=~\"$job\", instance=~\"$instance\"} == 0)^0", "format": "time_series", "instant": false, "legendFormat": "{{instance}}", @@ -407,7 +407,7 @@ ], "targets": [ { - "expr": "ssl_tls_connect_success{instance=~\"$instance\",job=~\"$job\"} == 0", + "expr": "ssl_probe_success{instance=~\"$instance\",job=~\"$job\"} == 0", "format": "table", "instant": true, "intervalFactor": 1, @@ -585,14 +585,14 @@ "value": ["$__all"] }, "datasource": "Prometheus", - "definition": "label_values(ssl_tls_connect_success, job)", + "definition": "label_values(ssl_probe_success, job)", "hide": 0, "includeAll": true, "label": "Job", "multi": true, "name": "job", "options": [], - "query": "label_values(ssl_tls_connect_success, job)", + "query": "label_values(ssl_probe_success, job)", "refresh": 1, "regex": "", "skipUrlSync": false, diff --git a/prober/file.go b/prober/file.go new file mode 100644 index 0000000..8206cd5 --- /dev/null +++ b/prober/file.go @@ -0,0 +1,35 @@ +package prober + +import ( + "context" + "fmt" + + "github.com/bmatcuk/doublestar/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/ribbybibby/ssl_exporter/config" +) + +func ProbeFile(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error { + errCh := make(chan error, 1) + + go func() { + files, err := doublestar.Glob(target) + if err != nil { + errCh <- err + return + } + + if len(files) == 0 { + errCh <- fmt.Errorf("No files found") + } else { + errCh <- collectFileMetrics(files, registry) + } + }() + + select { + case <-ctx.Done(): + return fmt.Errorf("context timeout, ran out of time") + case err := <-errCh: + return err + } +} diff --git a/prober/file_test.go b/prober/file_test.go new file mode 100644 index 0000000..6433220 --- /dev/null +++ b/prober/file_test.go @@ -0,0 +1,279 @@ +package prober + +import ( + "context" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ribbybibby/ssl_exporter/config" + "github.com/ribbybibby/ssl_exporter/test" + + "github.com/prometheus/client_golang/prometheus" + + dto "github.com/prometheus/client_model/go" +) + +// TestProbeFile tests a file +func TestProbeFile(t *testing.T) { + cert, certFile, err := createTestFile("", "tls*.crt") + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(certFile) + + module := config.Module{} + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeFile(ctx, certFile, module, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkFileMetrics(cert, certFile, registry, t) +} + +// TestProbeFileGlob tests matching a file with a glob +func TestProbeFileGlob(t *testing.T) { + cert, certFile, err := createTestFile("", "tls*.crt") + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(certFile) + + module := config.Module{} + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + glob := filepath.Dir(certFile) + "/*.crt" + + if err := ProbeFile(ctx, glob, module, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkFileMetrics(cert, certFile, registry, t) +} + +// TestProbeFileGlobDoubleStar tests matching a file with a ** glob +func TestProbeFileGlobDoubleStar(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "testdir") + if err != nil { + t.Fatalf(err.Error()) + } + cert, certFile, err := createTestFile(tmpDir, "tls*.crt") + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(certFile) + + module := config.Module{} + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + glob := filepath.Dir(filepath.Dir(certFile)) + "/**/*.crt" + + if err := ProbeFile(ctx, glob, module, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkFileMetrics(cert, certFile, registry, t) +} + +// TestProbeFileGlobDoubleStarMultiple tests matching multiple files with a ** glob +func TestProbeFileGlobDoubleStarMultiple(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "testdir") + if err != nil { + t.Fatalf(err.Error()) + } + defer os.RemoveAll(tmpDir) + + tmpDir1, err := ioutil.TempDir(tmpDir, "testdir") + if err != nil { + t.Fatalf(err.Error()) + } + cert1, certFile1, err := createTestFile(tmpDir1, "1*.crt") + if err != nil { + t.Fatalf(err.Error()) + } + + tmpDir2, err := ioutil.TempDir(tmpDir, "testdir") + if err != nil { + t.Fatalf(err.Error()) + } + cert2, certFile2, err := createTestFile(tmpDir2, "2*.crt") + if err != nil { + t.Fatalf(err.Error()) + } + + module := config.Module{} + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + glob := tmpDir + "/**/*.crt" + + if err := ProbeFile(ctx, glob, module, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkFileMetrics(cert1, certFile1, registry, t) + checkFileMetrics(cert2, certFile2, registry, t) +} + +// Create a certificate and write it to a file +func createTestFile(dir, filename string) (*x509.Certificate, string, error) { + certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1)) + block, _ := pem.Decode([]byte(certPEM)) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, "", err + } + tmpFile, err := ioutil.TempFile(dir, filename) + if err != nil { + return nil, tmpFile.Name(), err + } + if _, err := tmpFile.Write(certPEM); err != nil { + return nil, tmpFile.Name(), err + } + if err := tmpFile.Close(); err != nil { + return nil, tmpFile.Name(), err + } + + return cert, tmpFile.Name(), nil +} + +// Check metrics +func checkFileMetrics(cert *x509.Certificate, certFile string, registry *prometheus.Registry, t *testing.T) { + mfs, err := registry.Gather() + if err != nil { + t.Fatal(err) + } + + ips := "," + for _, ip := range cert.IPAddresses { + ips = ips + ip.String() + "," + } + expectedLabels := map[string]map[string]map[string]string{ + certFile: { + "ssl_file_cert_not_after": { + "file": certFile, + "serial_no": cert.SerialNumber.String(), + "issuer_cn": cert.Issuer.CommonName, + "cn": cert.Subject.CommonName, + "dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",", + "ips": ips, + "emails": "," + strings.Join(cert.EmailAddresses, ",") + ",", + "ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",", + }, + "ssl_file_cert_not_before": { + "file": certFile, + "serial_no": cert.SerialNumber.String(), + "issuer_cn": cert.Issuer.CommonName, + "cn": cert.Subject.CommonName, + "dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",", + "ips": ips, + "emails": "," + strings.Join(cert.EmailAddresses, ",") + ",", + "ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",", + }, + }, + } + checkFileRegistryLabels(expectedLabels, mfs, t) + + expectedResults := map[string]map[string]float64{ + certFile: { + "ssl_file_cert_not_after": float64(cert.NotAfter.Unix()), + "ssl_file_cert_not_before": float64(cert.NotBefore.Unix()), + }, + } + checkFileRegistryResults(expectedResults, mfs, t) +} + +// Check if expected results are in the registry +func checkFileRegistryResults(expRes map[string]map[string]float64, mfs []*dto.MetricFamily, t *testing.T) { + results := make(map[string]map[string]float64) + for _, mf := range mfs { + for _, metric := range mf.Metric { + for _, l := range metric.GetLabel() { + if l.GetName() == "file" { + if _, ok := results[l.GetValue()]; !ok { + results[l.GetValue()] = make(map[string]float64) + } + results[l.GetValue()][mf.GetName()] = metric.GetGauge().GetValue() + } + } + } + } + for expf, expr := range expRes { + for expm, expv := range expr { + if _, ok := results[expf]; !ok { + t.Fatalf("Could not find results for file %v", expf) + } + v, ok := results[expf][expm] + if !ok { + t.Fatalf("Expected metric %v not found in returned metrics for file %v", expm, expf) + } + if v != expv { + t.Fatalf("Expected: %v: %v, got: %v: %v for file %v", expm, expv, expm, v, expf) + } + } + } +} + +// Check if expected labels are in the registry +func checkFileRegistryLabels(expRes map[string]map[string]map[string]string, mfs []*dto.MetricFamily, t *testing.T) { + results := make(map[string]map[string]map[string]string) + for _, mf := range mfs { + for _, metric := range mf.Metric { + for _, l := range metric.GetLabel() { + if l.GetName() == "file" { + if _, ok := results[l.GetValue()]; !ok { + results[l.GetValue()] = make(map[string]map[string]string) + } + results[l.GetValue()][mf.GetName()] = make(map[string]string) + for _, sl := range metric.GetLabel() { + results[l.GetValue()][mf.GetName()][sl.GetName()] = sl.GetValue() + } + } + } + } + } + for expf, expr := range expRes { + for expm, expl := range expr { + if _, ok := results[expf]; !ok { + t.Fatalf("Could not find results for file %v", expf) + } + l, ok := results[expf][expm] + if !ok { + t.Fatalf("Expected metric %v not found in returned metrics for file %v", expm, expf) + } + for expk, expv := range expl { + v, ok := l[expk] + if !ok { + t.Fatalf("Expected label %v for metric %v not found in returned metrics for file %v", expk, expm, expf) + } + if v != expv { + t.Fatalf("Expected %v{%q=%q}, got: %v{%q=%q} for file %v", expm, expk, expv, expm, expk, v, expf) + } + } + if len(l) != len(expl) { + t.Fatalf("Expected %v labels but got %v for metric %v and file %v", len(expl), len(l), expm, expf) + } + } + } +} diff --git a/prober/https.go b/prober/https.go index 29df8c5..0d9b8e7 100644 --- a/prober/https.go +++ b/prober/https.go @@ -1,13 +1,13 @@ package prober import ( + "context" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" - "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" @@ -15,7 +15,7 @@ import ( ) // ProbeHTTPS performs a https probe -func ProbeHTTPS(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error { +func ProbeHTTPS(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error { tlsConfig, err := newTLSConfig("", registry, &module.TLSConfig) if err != nil { return err @@ -48,11 +48,15 @@ func ProbeHTTPS(target string, module config.Module, timeout time.Duration, regi Proxy: proxy, DisableKeepAlives: true, }, - Timeout: timeout, } // Issue a GET request to the target - resp, err := client.Get(targetURL.String()) + request, err := http.NewRequest(http.MethodGet, targetURL.String(), nil) + if err != nil { + return err + } + request = request.WithContext(ctx) + resp, err := client.Do(request) if err != nil { return err } diff --git a/prober/https_test.go b/prober/https_test.go index c20b75e..2e70103 100644 --- a/prober/https_test.go +++ b/prober/https_test.go @@ -1,6 +1,7 @@ package prober import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -37,7 +38,10 @@ func TestProbeHTTPS(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil { t.Fatalf("error: %s", err) } @@ -69,7 +73,10 @@ func TestProbeHTTPSInvalidName(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, "https://localhost:"+u.Port(), module, registry); err == nil { t.Fatalf("expected error, but err was nil") } } @@ -100,7 +107,10 @@ func TestProbeHTTPSNoScheme(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(u.Host, module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, u.Host, module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -132,7 +142,10 @@ func TestProbeHTTPSServerName(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, "https://localhost:"+u.Port(), module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -147,7 +160,10 @@ func TestProbeHTTPSHTTP(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, config.Module{}, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, config.Module{}, registry); err == nil { t.Fatalf("expected error, but err was nil") } } @@ -196,7 +212,10 @@ func TestProbeHTTPSClientAuth(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -249,7 +268,10 @@ func TestProbeHTTPSClientAuthWrongClientCert(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -282,7 +304,10 @@ func TestProbeHTTPSExpired(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -316,7 +341,10 @@ func TestProbeHTTPSExpiredInsecure(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -362,14 +390,17 @@ func TestProbeHTTPSProxy(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPS(ctx, server.URL, module, registry); err == nil { t.Fatalf("expected error but err was nil") } // Test with the proxy url, this shouldn't return an error module.HTTPS.ProxyURL = config.URL{URL: proxyURL} - if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { + if err := ProbeHTTPS(ctx, server.URL, module, registry); err != nil { t.Fatalf("error: %s", err) } diff --git a/prober/metrics.go b/prober/metrics.go index c430f66..63f03b2 100644 --- a/prober/metrics.go +++ b/prober/metrics.go @@ -3,12 +3,16 @@ package prober import ( "crypto/tls" "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" "sort" "strconv" "strings" "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" "golang.org/x/crypto/ocsp" ) @@ -82,7 +86,13 @@ func collectCertificateMetrics(certs []*x509.Certificate, registry *prometheus.R ) registry.MustRegister(notAfter, notBefore) - for _, cert := range uniq(certs) { + certs = uniq(certs) + + if len(certs) == 0 { + return fmt.Errorf("No certificates found") + } + + for _, cert := range certs { labels := labelValues(cert) if !cert.NotAfter.IsZero() { @@ -219,6 +229,65 @@ func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) erro return nil } +func collectFileMetrics(files []string, registry *prometheus.Registry) error { + var ( + totalCerts []*x509.Certificate + fileNotAfter = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "file_cert_not_after"), + Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a file", + }, + []string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + fileNotBefore = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "file_cert_not_before"), + Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a file", + }, + []string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + ) + registry.MustRegister(fileNotAfter, fileNotBefore) + + for _, f := range files { + data, err := ioutil.ReadFile(f) + if err != nil { + log.Debugf("Error reading file: %s error=%s", f, err) + continue + } + var certs []*x509.Certificate + for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + if !contains(certs, cert) { + certs = append(certs, cert) + } + } + } + totalCerts = append(totalCerts, certs...) + for _, cert := range certs { + labels := append([]string{f}, labelValues(cert)...) + + if !cert.NotAfter.IsZero() { + fileNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix())) + } + + if !cert.NotBefore.IsZero() { + fileNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix())) + } + } + } + + if len(totalCerts) == 0 { + return fmt.Errorf("No certificates found") + } + + return nil +} + func labelValues(cert *x509.Certificate) []string { return []string{ cert.SerialNumber.String(), diff --git a/prober/prober.go b/prober/prober.go index 179b828..bdd3c93 100644 --- a/prober/prober.go +++ b/prober/prober.go @@ -1,7 +1,7 @@ package prober import ( - "time" + "context" "github.com/prometheus/client_golang/prometheus" "github.com/ribbybibby/ssl_exporter/config" @@ -13,8 +13,9 @@ var ( "https": ProbeHTTPS, "http": ProbeHTTPS, "tcp": ProbeTCP, + "file": ProbeFile, } ) // ProbeFn probes -type ProbeFn func(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error +type ProbeFn func(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error diff --git a/prober/tcp.go b/prober/tcp.go index e669d3f..260c2a6 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -2,11 +2,11 @@ package prober import ( "bufio" + "context" "crypto/tls" "fmt" "net" "regexp" - "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" @@ -14,21 +14,21 @@ import ( ) // ProbeTCP performs a tcp probe -func ProbeTCP(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error { +func ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry) error { tlsConfig, err := newTLSConfig(target, registry, &module.TLSConfig) if err != nil { return err } - dialer := &net.Dialer{Timeout: timeout} - - conn, err := dialer.Dial("tcp", target) + dialer := &net.Dialer{} + conn, err := dialer.DialContext(ctx, "tcp", target) if err != nil { return err } defer conn.Close() - if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { + deadline, _ := ctx.Deadline() + if err := conn.SetDeadline(deadline); err != nil { return fmt.Errorf("Error setting deadline") } diff --git a/prober/tcp_test.go b/prober/tcp_test.go index a64f6b7..c3a13a8 100644 --- a/prober/tcp_test.go +++ b/prober/tcp_test.go @@ -1,6 +1,7 @@ package prober import ( + "context" "crypto/tls" "net" "testing" @@ -33,7 +34,10 @@ func TestProbeTCP(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -61,7 +65,10 @@ func TestProbeTCPInvalidName(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, "localhost:"+listenPort, module, registry); err == nil { t.Fatalf("expected error but err was nil") } } @@ -90,7 +97,10 @@ func TestProbeTCPServerName(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, "localhost:"+listenPort, module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -123,7 +133,10 @@ func TestProbeTCPExpired(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -157,7 +170,10 @@ func TestProbeTCPExpiredInsecure(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil { t.Fatalf("error: %s", err) } @@ -186,7 +202,10 @@ func TestProbeTCPStartTLSSMTP(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -214,7 +233,10 @@ func TestProbeTCPStartTLSFTP(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -242,7 +264,10 @@ func TestProbeTCPStartTLSIMAP(t *testing.T) { registry := prometheus.NewRegistry() - if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeTCP(ctx, server.Listener.Addr().String(), module, registry); err != nil { t.Fatalf("error: %s", err) } } diff --git a/ssl_exporter.go b/ssl_exporter.go index 1314091..73d528f 100644 --- a/ssl_exporter.go +++ b/ssl_exporter.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "net/http" "strconv" @@ -52,6 +53,9 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) { timeout = time.Duration((timeoutSeconds) * 1e9) } + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer cancel() + target := r.URL.Query().Get("target") if target == "" { http.Error(w, "Target parameter is missing", http.StatusBadRequest) @@ -65,10 +69,10 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) { } var ( - tlsConnectSuccess = prometheus.NewGauge( + probeSuccess = prometheus.NewGauge( prometheus.GaugeOpts{ - Name: prometheus.BuildFQName(namespace, "", "tls_connect_success"), - Help: "If the TLS connection was a success", + Name: prometheus.BuildFQName(namespace, "", "probe_success"), + Help: "If the probe was a success", }, ) proberType = prometheus.NewGaugeVec( @@ -81,16 +85,15 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) { ) registry := prometheus.NewRegistry() - registry.MustRegister(tlsConnectSuccess, proberType) + registry.MustRegister(probeSuccess, proberType) proberType.WithLabelValues(module.Prober).Set(1) - err := probeFunc(target, module, timeout, registry) + err := probeFunc(ctx, target, module, registry) if err != nil { log.Errorf("error=%s target=%s prober=%s timeout=%s", err, target, module.Prober, timeout) - tlsConnectSuccess.Set(0) - + probeSuccess.Set(0) } else { - tlsConnectSuccess.Set(1) + probeSuccess.Set(1) } // Serve diff --git a/ssl_exporter_test.go b/ssl_exporter_test.go index 146b7e2..41a16e0 100644 --- a/ssl_exporter_test.go +++ b/ssl_exporter_test.go @@ -52,8 +52,8 @@ func TestProbeHandlerHTTPS(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } // Check probe metric @@ -112,8 +112,8 @@ func TestProbeHandlerHTTPSTimeout(t *testing.T) { t.Fatalf(err.Error()) } - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok { + t.Errorf("expected `ssl_probe_success 0`") } if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok { @@ -211,8 +211,8 @@ func TestProbeHandlerHTTPSNoServer(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok { + t.Errorf("expected `ssl_probe_success 0`") } } @@ -235,9 +235,9 @@ func TestProbeHandlerHTTPSSpaces(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -260,9 +260,9 @@ func TestProbeHandlerHTTPSHTTP(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -319,9 +319,9 @@ func TestProbeHandlerHTTPSClientAuthWrongClientCert(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -353,8 +353,8 @@ func TestProbeHandlerTCP(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } // Check probe metric @@ -402,8 +402,8 @@ func TestProbeHandlerTCPTimeout(t *testing.T) { t.Fatalf(err.Error()) } - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok { + t.Errorf("expected `ssl_probe_success 0`") } if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok { @@ -583,8 +583,8 @@ func TestProbeHandlerTCPNoServer(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok { + t.Errorf("expected `ssl_probe_success 0`") } } @@ -607,9 +607,9 @@ func TestProbeHandlerTCPSpaces(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -632,9 +632,9 @@ func TestProbeHandlerTCPHTTP(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -672,9 +672,9 @@ func TestProbeHandlerTCPExpired(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0") if !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + t.Errorf("expected `ssl_probe_success 0`") } } @@ -713,9 +713,9 @@ func TestProbeHandlerTCPExpiredInsecure(t *testing.T) { t.Fatalf(err.Error()) } - ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1") + ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1") if !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + t.Errorf("expected `ssl_probe_success 1`") } } @@ -769,8 +769,8 @@ func TestProbeHandlerProxy(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 0"); !ok { - t.Errorf("expected `ssl_tls_connect_success 0`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok { + t.Errorf("expected `ssl_probe_success 0`") } // Test with an actual proxy server @@ -808,8 +808,8 @@ func TestProbeHandlerProxy(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } } @@ -844,8 +844,8 @@ func TestProbeHandlerTCPStartTLSSMTP(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } // Check probe metric @@ -890,8 +890,8 @@ func TestProbeHandlerTCPStartTLSFTP(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } // Check probe metric @@ -936,8 +936,8 @@ func TestProbeHandlerTCPStartTLSIMAP(t *testing.T) { } // Check success metric - if ok := strings.Contains(rr.Body.String(), "ssl_tls_connect_success 1"); !ok { - t.Errorf("expected `ssl_tls_connect_success 1`") + if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok { + t.Errorf("expected `ssl_probe_success 1`") } // Check probe metric diff --git a/vendor/github.com/bmatcuk/doublestar/v2/.gitignore b/vendor/github.com/bmatcuk/doublestar/v2/.gitignore new file mode 100644 index 0000000..af212ec --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/.gitignore @@ -0,0 +1,32 @@ +# vi +*~ +*.swp +*.swo + +# 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 +*.test +*.prof + +# test directory +test/ diff --git a/vendor/github.com/bmatcuk/doublestar/v2/.travis.yml b/vendor/github.com/bmatcuk/doublestar/v2/.travis.yml new file mode 100644 index 0000000..78a90e7 --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - 1.12 + - 1.13 + - 1.14 + +os: + - linux + - windows + +before_install: + - go get -t -v ./... + +script: + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) + diff --git a/vendor/github.com/bmatcuk/doublestar/v2/LICENSE b/vendor/github.com/bmatcuk/doublestar/v2/LICENSE new file mode 100644 index 0000000..309c9d1 --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Bob Matcuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/bmatcuk/doublestar/v2/README.md b/vendor/github.com/bmatcuk/doublestar/v2/README.md new file mode 100644 index 0000000..1c958d5 --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/README.md @@ -0,0 +1,143 @@ +# doublestar + +Path pattern matching and globbing supporting `doublestar` (`**`) patterns. + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/bmatcuk/doublestar)](https://pkg.go.dev/github.com/bmatcuk/doublestar/v2) +[![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master)](https://github.com/bmatcuk/doublestar/releases) +[![Build Status](https://travis-ci.org/bmatcuk/doublestar.svg?branch=master)](https://travis-ci.org/bmatcuk/doublestar) +[![codecov.io](https://img.shields.io/codecov/c/github/bmatcuk/doublestar.svg?branch=master)](https://codecov.io/github/bmatcuk/doublestar?branch=master) + +## About + +#### [Updating from v1 to v2?](UPGRADING.md) + +**doublestar** is a [golang](http://golang.org/) implementation of path pattern +matching and globbing with support for "doublestar" (aka globstar: `**`) +patterns. + +doublestar patterns match files and directories recursively. For example, if +you had the following directory structure: + +```bash +grandparent +`-- parent + |-- child1 + `-- child2 +``` + +You could find the children with patterns such as: `**/child*`, +`grandparent/**/child?`, `**/parent/*`, or even just `**` by itself (which will +return all files and directories recursively). + +Bash's globstar is doublestar's inspiration and, as such, works similarly. +Note that the doublestar must appear as a path component by itself. A pattern +such as `/path**` is invalid and will be treated the same as `/path*`, but +`/path*/**` should achieve the desired result. Additionally, `/path/**` will +match all directories and files under the path directory, but `/path/**/` will +only match directories. + +## Installation + +**doublestar** can be installed via `go get`: + +```bash +go get github.com/bmatcuk/doublestar/v2 +``` + +To use it in your code, you must import it: + +```go +import "github.com/bmatcuk/doublestar/v2" +``` + +## Usage + +### Match + +```go +func Match(pattern, name string) (bool, error) +``` + +Match returns true if `name` matches the file name `pattern` +([see below](#patterns)). `name` and `pattern` are split on forward slash (`/`) +characters and may be relative or absolute. + +Note: `Match()` is meant to be a drop-in replacement for `path.Match()`. As +such, it always uses `/` as the path separator. If you are writing code that +will run on systems where `/` is not the path separator (such as Windows), you +want to use `PathMatch()` (below) instead. + + +### PathMatch + +```go +func PathMatch(pattern, name string) (bool, error) +``` + +PathMatch returns true if `name` matches the file name `pattern` +([see below](#patterns)). The difference between Match and PathMatch is that +PathMatch will automatically use your system's path separator to split `name` +and `pattern`. + +`PathMatch()` is meant to be a drop-in replacement for `filepath.Match()`. + +### Glob + +```go +func Glob(pattern string) ([]string, error) +``` + +Glob finds all files and directories in the filesystem that match `pattern` +([see below](#patterns)). `pattern` may be relative (to the current working +directory), or absolute. + +`Glob()` is meant to be a drop-in replacement for `filepath.Glob()`. + +### Patterns + +**doublestar** supports the following special terms in the patterns: + +Special Terms | Meaning +------------- | ------- +`*` | matches any sequence of non-path-separators +`**` | matches any sequence of characters, including path separators +`?` | matches any single non-path-separator character +`[class]` | matches any single non-path-separator character against a class of characters ([see below](#character-classes)) +`{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches + +Any character with a special meaning can be escaped with a backslash (`\`). + +#### Character Classes + +Character classes support the following: + +Class | Meaning +---------- | ------- +`[abc]` | matches any single character within the set +`[a-z]` | matches any single character in the range +`[^class]` | matches any single character which does *not* match the class + +### Abstracting the `os` package + +**doublestar** by default uses the `Open`, `Stat`, and `Lstat`, functions and +`PathSeparator` value from the standard library's `os` package. To abstract +this, for example to be able to perform tests of Windows paths on Linux, or to +interoperate with your own filesystem code, it includes the functions `GlobOS` +and `PathMatchOS` which are identical to `Glob` and `PathMatch` except that they +operate on an `OS` interface: + +```go +type OS interface { + Lstat(name string) (os.FileInfo, error) + Open(name string) (*os.File, error) + PathSeparator() rune + Stat(name string) (os.FileInfo, error) +} +``` + +`StandardOS` is a value that implements this interface by calling functions in +the standard library's `os` package. + +## License + +[MIT License](LICENSE) diff --git a/vendor/github.com/bmatcuk/doublestar/v2/UPGRADING.md b/vendor/github.com/bmatcuk/doublestar/v2/UPGRADING.md new file mode 100644 index 0000000..8193544 --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/UPGRADING.md @@ -0,0 +1,13 @@ +# Upgrading from v1 to v2 + +The change from v1 to v2 was fairly minor: the return type of the `Open` method +on the `OS` interface was changed from `*os.File` to `File`, a new interface +exported by doublestar. The new `File` interface only defines the functionality +doublestar actually needs (`io.Closer` and `Readdir`), making it easier to use +doublestar with [go-billy](https://github.com/src-d/go-billy), +[afero](https://github.com/spf13/afero), or something similar. If you were +using this functionality, updating should be as easy as updating `Open's` +return type, since `os.File` already implements `doublestar.File`. + +If you weren't using this functionality, updating should be as easy as changing +your dependencies to point to v2. diff --git a/vendor/github.com/bmatcuk/doublestar/v2/doublestar.go b/vendor/github.com/bmatcuk/doublestar/v2/doublestar.go new file mode 100644 index 0000000..3e5bbf7 --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/doublestar.go @@ -0,0 +1,630 @@ +package doublestar + +import ( + "fmt" + "io" + "os" + "path" + "path/filepath" + "sort" + "strings" + "unicode/utf8" +) + +// File defines a subset of file operations +type File interface { + io.Closer + Readdir(count int) ([]os.FileInfo, error) +} + +// An OS abstracts functions in the standard library's os package. +type OS interface { + Lstat(name string) (os.FileInfo, error) + Open(name string) (File, error) + PathSeparator() rune + Stat(name string) (os.FileInfo, error) +} + +// A standardOS implements OS by calling functions in the standard library's os +// package. +type standardOS struct{} + +func (standardOS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) } +func (standardOS) Open(name string) (File, error) { return os.Open(name) } +func (standardOS) PathSeparator() rune { return os.PathSeparator } +func (standardOS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } + +// StandardOS is a value that implements the OS interface by calling functions +// in the standard libray's os package. +var StandardOS OS = standardOS{} + +// ErrBadPattern indicates a pattern was malformed. +var ErrBadPattern = path.ErrBadPattern + +// Find the first index of a rune in a string, +// ignoring any times the rune is escaped using "\". +func indexRuneWithEscaping(s string, r rune) int { + end := strings.IndexRune(s, r) + if end == -1 || r == '\\' { + return end + } + if end > 0 && s[end-1] == '\\' { + start := end + utf8.RuneLen(r) + end = indexRuneWithEscaping(s[start:], r) + if end != -1 { + end += start + } + } + return end +} + +// Find the last index of a rune in a string, +// ignoring any times the rune is escaped using "\". +func lastIndexRuneWithEscaping(s string, r rune) int { + end := strings.LastIndex(s, string(r)) + if end == -1 { + return -1 + } + if end > 0 && s[end-1] == '\\' { + end = lastIndexRuneWithEscaping(s[:end-1], r) + } + return end +} + +// Find the index of the first instance of one of the unicode characters in +// chars, ignoring any times those characters are escaped using "\". +func indexAnyWithEscaping(s, chars string) int { + end := strings.IndexAny(s, chars) + if end == -1 { + return -1 + } + if end > 0 && s[end-1] == '\\' { + _, adj := utf8.DecodeRuneInString(s[end:]) + start := end + adj + end = indexAnyWithEscaping(s[start:], chars) + if end != -1 { + end += start + } + } + return end +} + +// Split a set of alternatives such as {alt1,alt2,...} and returns the index of +// the rune after the closing curly brace. Respects nested alternatives and +// escaped runes. +func splitAlternatives(s string) (ret []string, idx int) { + ret = make([]string, 0, 2) + idx = 0 + slen := len(s) + braceCnt := 1 + esc := false + start := 0 + for braceCnt > 0 { + if idx >= slen { + return nil, -1 + } + + sRune, adj := utf8.DecodeRuneInString(s[idx:]) + if esc { + esc = false + } else if sRune == '\\' { + esc = true + } else if sRune == '{' { + braceCnt++ + } else if sRune == '}' { + braceCnt-- + } else if sRune == ',' && braceCnt == 1 { + ret = append(ret, s[start:idx]) + start = idx + adj + } + + idx += adj + } + ret = append(ret, s[start:idx-1]) + return +} + +// Returns true if the pattern is "zero length", meaning +// it could match zero or more characters. +func isZeroLengthPattern(pattern string) (ret bool, err error) { + // * can match zero + if pattern == "" || pattern == "*" || pattern == "**" { + return true, nil + } + + // an alternative with zero length can match zero, for example {,x} - the + // first alternative has zero length + r, adj := utf8.DecodeRuneInString(pattern) + if r == '{' { + options, endOptions := splitAlternatives(pattern[adj:]) + if endOptions == -1 { + return false, ErrBadPattern + } + if ret, err = isZeroLengthPattern(pattern[adj+endOptions:]); !ret || err != nil { + return + } + for _, o := range options { + if ret, err = isZeroLengthPattern(o); ret || err != nil { + return + } + } + } + + return false, nil +} + +// Match returns true if name matches the shell file name pattern. +// The pattern syntax is: +// +// pattern: +// { term } +// term: +// '*' matches any sequence of non-path-separators +// '**' matches any sequence of characters, including +// path separators. +// '?' matches any single non-path-separator character +// '[' [ '^' ] { character-range } ']' +// character class (must be non-empty) +// '{' { term } [ ',' { term } ... ] '}' +// c matches character c (c != '*', '?', '\\', '[') +// '\\' c matches character c +// +// character-range: +// c matches character c (c != '\\', '-', ']') +// '\\' c matches character c +// lo '-' hi matches character c for lo <= c <= hi +// +// Match requires pattern to match all of name, not just a substring. +// The path-separator defaults to the '/' character. The only possible +// returned error is ErrBadPattern, when pattern is malformed. +// +// Note: this is meant as a drop-in replacement for path.Match() which +// always uses '/' as the path separator. If you want to support systems +// which use a different path separator (such as Windows), what you want +// is the PathMatch() function below. +// +func Match(pattern, name string) (bool, error) { + return doMatching(pattern, name, '/') +} + +// PathMatch is like Match except that it uses your system's path separator. +// For most systems, this will be '/'. However, for Windows, it would be '\\'. +// Note that for systems where the path separator is '\\', escaping is +// disabled. +// +// Note: this is meant as a drop-in replacement for filepath.Match(). +// +func PathMatch(pattern, name string) (bool, error) { + return PathMatchOS(StandardOS, pattern, name) +} + +// PathMatchOS is like PathMatch except that it uses vos's path separator. +func PathMatchOS(vos OS, pattern, name string) (bool, error) { + pattern = filepath.ToSlash(pattern) + return doMatching(pattern, name, vos.PathSeparator()) +} + +func doMatching(pattern, name string, separator rune) (matched bool, err error) { + // check for some base-cases + patternLen, nameLen := len(pattern), len(name) + if patternLen == 0 { + return nameLen == 0, nil + } else if nameLen == 0 { + return isZeroLengthPattern(pattern) + } + + separatorAdj := utf8.RuneLen(separator) + + patIdx := indexRuneWithEscaping(pattern, '/') + lastPat := patIdx == -1 + if lastPat { + patIdx = len(pattern) + } + if pattern[:patIdx] == "**" { + // if our last pattern component is a doublestar, we're done - + // doublestar will match any remaining name components, if any. + if lastPat { + return true, nil + } + + // otherwise, try matching remaining components + nameIdx := 0 + patIdx += 1 + for { + if m, _ := doMatching(pattern[patIdx:], name[nameIdx:], separator); m { + return true, nil + } + + nextNameIdx := 0 + if nextNameIdx = indexRuneWithEscaping(name[nameIdx:], separator); nextNameIdx == -1 { + break + } + nameIdx += separatorAdj + nextNameIdx + } + return false, nil + } + + nameIdx := indexRuneWithEscaping(name, separator) + lastName := nameIdx == -1 + if lastName { + nameIdx = nameLen + } + + var matches []string + matches, err = matchComponent(pattern, name[:nameIdx]) + if matches == nil || err != nil { + return + } + if len(matches) == 0 && lastName { + return true, nil + } + + if !lastName { + nameIdx += separatorAdj + for _, alt := range matches { + matched, err = doMatching(alt, name[nameIdx:], separator) + if matched || err != nil { + return + } + } + } + + return false, nil +} + +// Glob returns the names of all files matching pattern or nil +// if there is no matching file. The syntax of pattern is the same +// as in Match. The pattern may describe hierarchical names such as +// /usr/*/bin/ed (assuming the Separator is '/'). +// +// Glob ignores file system errors such as I/O errors reading directories. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +// +// Your system path separator is automatically used. This means on +// systems where the separator is '\\' (Windows), escaping will be +// disabled. +// +// Note: this is meant as a drop-in replacement for filepath.Glob(). +// +func Glob(pattern string) (matches []string, err error) { + return GlobOS(StandardOS, pattern) +} + +// GlobOS is like Glob except that it operates on vos. +func GlobOS(vos OS, pattern string) (matches []string, err error) { + if len(pattern) == 0 { + return nil, nil + } + + // if the pattern starts with alternatives, we need to handle that here - the + // alternatives may be a mix of relative and absolute + if pattern[0] == '{' { + options, endOptions := splitAlternatives(pattern[1:]) + if endOptions == -1 { + return nil, ErrBadPattern + } + for _, o := range options { + m, e := Glob(o + pattern[endOptions+1:]) + if e != nil { + return nil, e + } + matches = append(matches, m...) + } + return matches, nil + } + + // If the pattern is relative or absolute and we're on a non-Windows machine, + // volumeName will be an empty string. If it is absolute and we're on a + // Windows machine, volumeName will be a drive letter ("C:") for filesystem + // paths or \\\ for UNC paths. + isAbs := filepath.IsAbs(pattern) || pattern[0] == '\\' || pattern[0] == '/' + volumeName := filepath.VolumeName(pattern) + isWindowsUNC := strings.HasPrefix(volumeName, `\\`) + if isWindowsUNC || isAbs { + startIdx := len(volumeName) + 1 + return doGlob(vos, fmt.Sprintf("%s%s", volumeName, string(vos.PathSeparator())), filepath.ToSlash(pattern[startIdx:]), matches) + } + + // otherwise, it's a relative pattern + return doGlob(vos, ".", filepath.ToSlash(pattern), matches) +} + +// Perform a glob +func doGlob(vos OS, basedir, pattern string, matches []string) (m []string, e error) { + m = matches + e = nil + + // if the pattern starts with any path components that aren't globbed (ie, + // `path/to/glob*`), we can skip over the un-globbed components (`path/to` in + // our example). + globIdx := indexAnyWithEscaping(pattern, "*?[{\\") + if globIdx > 0 { + globIdx = lastIndexRuneWithEscaping(pattern[:globIdx], '/') + } else if globIdx == -1 { + globIdx = lastIndexRuneWithEscaping(pattern, '/') + } + if globIdx > 0 { + basedir = filepath.Join(basedir, pattern[:globIdx]) + pattern = pattern[globIdx+1:] + } + + // Lstat will return an error if the file/directory doesn't exist + fi, err := vos.Lstat(basedir) + if err != nil { + return + } + + // if the pattern is empty, we've found a match + if len(pattern) == 0 { + m = append(m, basedir) + return + } + + // otherwise, we need to check each item in the directory... + + // first, if basedir is a symlink, follow it... + if (fi.Mode() & os.ModeSymlink) != 0 { + fi, err = vos.Stat(basedir) + if err != nil { + return + } + } + + // confirm it's a directory... + if !fi.IsDir() { + return + } + + files, err := filesInDir(vos, basedir) + if err != nil { + return + } + + sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) + + slashIdx := indexRuneWithEscaping(pattern, '/') + lastComponent := slashIdx == -1 + if lastComponent { + slashIdx = len(pattern) + } + if pattern[:slashIdx] == "**" { + // if the current component is a doublestar, we'll try depth-first + for _, file := range files { + // if symlink, we may want to follow + if (file.Mode() & os.ModeSymlink) != 0 { + file, err = vos.Stat(filepath.Join(basedir, file.Name())) + if err != nil { + continue + } + } + + if file.IsDir() { + // recurse into directories + if lastComponent { + m = append(m, filepath.Join(basedir, file.Name())) + } + m, e = doGlob(vos, filepath.Join(basedir, file.Name()), pattern, m) + } else if lastComponent { + // if the pattern's last component is a doublestar, we match filenames, too + m = append(m, filepath.Join(basedir, file.Name())) + } + } + if lastComponent { + return // we're done + } + + pattern = pattern[slashIdx+1:] + } + + // check items in current directory and recurse + var match []string + for _, file := range files { + match, e = matchComponent(pattern, file.Name()) + if e != nil { + return + } + if match != nil { + if len(match) == 0 { + m = append(m, filepath.Join(basedir, file.Name())) + } else { + for _, alt := range match { + m, e = doGlob(vos, filepath.Join(basedir, file.Name()), alt, m) + } + } + } + } + return +} + +func filesInDir(vos OS, dirPath string) (files []os.FileInfo, e error) { + dir, err := vos.Open(dirPath) + if err != nil { + return nil, nil + } + defer func() { + if err := dir.Close(); e == nil { + e = err + } + }() + + files, err = dir.Readdir(-1) + if err != nil { + return nil, nil + } + + return +} + +// Attempt to match a single path component with a pattern. Note that the +// pattern may include multiple components but that the "name" is just a single +// path component. The return value is a slice of patterns that should be +// checked against subsequent path components or nil, indicating that the +// pattern does not match this path. It is assumed that pattern components are +// separated by '/' +func matchComponent(pattern, name string) ([]string, error) { + // check for matches one rune at a time + patternLen, nameLen := len(pattern), len(name) + patIdx, nameIdx := 0, 0 + for patIdx < patternLen && nameIdx < nameLen { + patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:]) + nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:]) + if patRune == '/' { + patIdx++ + break + } else if patRune == '\\' { + // handle escaped runes, only if separator isn't '\\' + patIdx += patAdj + patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]) + if patRune == utf8.RuneError { + return nil, ErrBadPattern + } else if patRune == nameRune { + patIdx += patAdj + nameIdx += nameAdj + } else { + return nil, nil + } + } else if patRune == '*' { + // handle stars - a star at the end of the pattern or before a separator + // will always match the rest of the path component + if patIdx += patAdj; patIdx >= patternLen { + return []string{}, nil + } + if patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:]); patRune == '/' { + return []string{pattern[patIdx+patAdj:]}, nil + } + + // check if we can make any matches + for ; nameIdx < nameLen; nameIdx += nameAdj { + if m, e := matchComponent(pattern[patIdx:], name[nameIdx:]); m != nil || e != nil { + return m, e + } + _, nameAdj = utf8.DecodeRuneInString(name[nameIdx:]) + } + return nil, nil + } else if patRune == '[' { + // handle character sets + patIdx += patAdj + endClass := indexRuneWithEscaping(pattern[patIdx:], ']') + if endClass == -1 { + return nil, ErrBadPattern + } + endClass += patIdx + classRunes := []rune(pattern[patIdx:endClass]) + classRunesLen := len(classRunes) + if classRunesLen > 0 { + classIdx := 0 + matchClass := false + if classRunes[0] == '^' { + classIdx++ + } + for classIdx < classRunesLen { + low := classRunes[classIdx] + if low == '-' { + return nil, ErrBadPattern + } + classIdx++ + if low == '\\' { + if classIdx < classRunesLen { + low = classRunes[classIdx] + classIdx++ + } else { + return nil, ErrBadPattern + } + } + high := low + if classIdx < classRunesLen && classRunes[classIdx] == '-' { + // we have a range of runes + if classIdx++; classIdx >= classRunesLen { + return nil, ErrBadPattern + } + high = classRunes[classIdx] + if high == '-' { + return nil, ErrBadPattern + } + classIdx++ + if high == '\\' { + if classIdx < classRunesLen { + high = classRunes[classIdx] + classIdx++ + } else { + return nil, ErrBadPattern + } + } + } + if low <= nameRune && nameRune <= high { + matchClass = true + } + } + if matchClass == (classRunes[0] == '^') { + return nil, nil + } + } else { + return nil, ErrBadPattern + } + patIdx = endClass + 1 + nameIdx += nameAdj + } else if patRune == '{' { + // handle alternatives such as {alt1,alt2,...} + patIdx += patAdj + options, endOptions := splitAlternatives(pattern[patIdx:]) + if endOptions == -1 { + return nil, ErrBadPattern + } + patIdx += endOptions + + results := make([][]string, 0, len(options)) + totalResults := 0 + for _, o := range options { + m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:]) + if e != nil { + return nil, e + } + if m != nil { + results = append(results, m) + totalResults += len(m) + } + } + if len(results) > 0 { + lst := make([]string, 0, totalResults) + for _, m := range results { + lst = append(lst, m...) + } + return lst, nil + } + + return nil, nil + } else if patRune == '?' || patRune == nameRune { + // handle single-rune wildcard + patIdx += patAdj + nameIdx += nameAdj + } else { + return nil, nil + } + } + if nameIdx >= nameLen { + if patIdx >= patternLen { + return []string{}, nil + } + + pattern = pattern[patIdx:] + slashIdx := indexRuneWithEscaping(pattern, '/') + testPattern := pattern + if slashIdx >= 0 { + testPattern = pattern[:slashIdx] + } + + zeroLength, err := isZeroLengthPattern(testPattern) + if err != nil { + return nil, err + } + if zeroLength { + if slashIdx == -1 { + return []string{}, nil + } else { + return []string{pattern[slashIdx+1:]}, nil + } + } + } + return nil, nil +} diff --git a/vendor/github.com/bmatcuk/doublestar/v2/go.mod b/vendor/github.com/bmatcuk/doublestar/v2/go.mod new file mode 100644 index 0000000..f0fa6bc --- /dev/null +++ b/vendor/github.com/bmatcuk/doublestar/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/bmatcuk/doublestar/v2 + +go 1.12 diff --git a/vendor/modules.txt b/vendor/modules.txt index e746358..579280a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -5,6 +5,9 @@ github.com/alecthomas/template/parse github.com/alecthomas/units # github.com/beorn7/perks v1.0.1 github.com/beorn7/perks/quantile +# github.com/bmatcuk/doublestar/v2 v2.0.3 +## explicit +github.com/bmatcuk/doublestar/v2 # github.com/cespare/xxhash/v2 v2.1.1 github.com/cespare/xxhash/v2 # github.com/golang/protobuf v1.4.3 @@ -25,6 +28,7 @@ github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/internal github.com/prometheus/client_golang/prometheus/promhttp # github.com/prometheus/client_model v0.2.0 +## explicit github.com/prometheus/client_model/go # github.com/prometheus/common v0.14.0 ## explicit