From c81b4df0f11c99ae35f73fe51464f75a7b9e06e4 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 13 Feb 2023 10:33:33 +0700 Subject: [PATCH] feat: support tls configuration --- README.md | 42 ++++++++++++++++++++++ config.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++---- config.yml | 3 ++ exporter.go | 12 +++++-- main.go | 11 ++++-- 5 files changed, 160 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d7273f9..dd36765 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,48 @@ then it's up and healthy. If the value is 0, then it's down. `uptime_latency_seconds` indicates the time it took to do a HTTP request to that specified `endpoint_address` field in second. +## Configuration file schema + +If you want to do config.json. + +```json +{ + "endpoints": [ + { + "name": "string", + "address": "https://url", + "method": "GET", + "timeout": 123, + "successful_status_code": "2xx", + "inverse_status": false, + "tls_configuration": { + "certificate_authority_path": "/path/to/certificate_authority.pem", + "client_certificate_path": "/path/to/client_certificate.pem", + "client_key_path": "/path/to/client_key.pem", + "insecure_skip_verify": true + } + } + ] +} +``` + +If you want to do config.yaml (or .yml if you prefer). + +```yaml +endpoints: + - name: "string" + address: "https://url" + method: "GET" + timeout: 123 + successful_status_code: 2xx + inverse_status: false + tls_configuration: + certificate_authority_path: "/path/to/certificate_authority.pem", + client_certificate_path: "/path/to/client_certificate.pem", + client_key_path: "/path/to/client_key.pem", + insecure_skip_verify: true +``` + ## Build from source Install [Go](https://go.dev/dl). diff --git a/config.go b/config.go index f72a429..63ce5c1 100644 --- a/config.go +++ b/config.go @@ -22,15 +22,105 @@ package main +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" +) + type Configuration struct { Endpoints []Endpoint `required:"true"` } type Endpoint struct { - Name string `required:"true"` - Address string `required:"true"` - Method string `required:"false" default:"GET"` - Timeout uint16 `required:"false" default:"30"` - SuccessfulStatusCode string `required:"false" default:"2xx"` - InverseStatus bool `required:"false" default:"false"` + Name string `required:"true"` + Address string `required:"true"` + Method string `required:"false" default:"GET"` + Timeout uint16 `required:"false" default:"30"` + SuccessfulStatusCode string `required:"false" default:"2xx" yaml:"successful_status_code" json:"successful_status_code" toml:"successful_status_code"` + InverseStatus bool `required:"false" default:"false" yaml:"inverse_status" json:"inverse_status" toml:"inverse_status"` + TLSConfiguration TLSConfiguration `required:"false" yaml:"tls_configuration" json:"tls_configuration" toml:"tls_configuration"` +} + +type TLSConfiguration struct { + CertificateAuthorityPath string `required:"false" yaml:"certificate_authority_path" json:"certificate_authority_path" toml:"certificate_authority_path"` + ClientCertificatePath string `required:"false" yaml:"client_certificate_path" json:"client_certificate_path" toml:"client_certificate_path"` + ClientKeyPath string `required:"false" yaml:"client_key_path" json:"client_key_path" toml:"client_key_path"` + InsecureSkipVerify bool `required:"false" default:"false" yaml:"insecure_skip_verify" json:"insecure_skip_verify" toml:"insecure_skip_verify"` +} + +type PreprocessedEndpoint struct { + Name string + Address string + Method string + Timeout uint16 + SuccessfulStatusCode string + InverseStatus bool + TLSConfiguration PreproccessedTLSConfiguration +} + +type PreproccessedTLSConfiguration struct { + Certificates []tls.Certificate + RootCA *x509.CertPool + InsecureSkipVerify bool +} + +func processConfiguration(endpoints []Endpoint) ([]PreprocessedEndpoint, error) { + var preprocessedEndpoints []PreprocessedEndpoint + + for _, endpoint := range endpoints { + var certificates []tls.Certificate = nil + var rootCA *x509.CertPool = nil + + if endpoint.TLSConfiguration.CertificateAuthorityPath != "" { + // Read CA file + file, err := os.ReadFile(endpoint.TLSConfiguration.CertificateAuthorityPath) + if err != nil { + return nil, fmt.Errorf("reading certificate authority file: %s", err.Error()) + } + + rootCA = x509.NewCertPool() + ok := rootCA.AppendCertsFromPEM(file) + if !ok { + return nil, fmt.Errorf("invalid pem format for certificate authority") + } + } + + if endpoint.TLSConfiguration.ClientCertificatePath != "" && endpoint.TLSConfiguration.ClientKeyPath != "" { + // Read certificate file + certificateFile, err := os.ReadFile(endpoint.TLSConfiguration.ClientCertificatePath) + if err != nil { + return nil, fmt.Errorf("reading certificate file: %s", err.Error()) + } + + keyFile, err := os.ReadFile(endpoint.TLSConfiguration.ClientKeyPath) + if err != nil { + return nil, fmt.Errorf("reading key file: %s", err.Error()) + } + + certificate, err := tls.X509KeyPair(certificateFile, keyFile) + if err != nil { + return nil, fmt.Errorf("creating x509 key pair: %s", err.Error()) + } + + certificates = append(certificates, certificate) + } + + preprocessedEndpoints = append(preprocessedEndpoints, PreprocessedEndpoint{ + Name: endpoint.Name, + Address: endpoint.Address, + Method: endpoint.Method, + Timeout: endpoint.Timeout, + SuccessfulStatusCode: endpoint.SuccessfulStatusCode, + InverseStatus: endpoint.InverseStatus, + TLSConfiguration: PreproccessedTLSConfiguration{ + Certificates: certificates, + RootCA: rootCA, + InsecureSkipVerify: endpoint.TLSConfiguration.InsecureSkipVerify, + }, + }) + } + + return preprocessedEndpoints, nil } diff --git a/config.yml b/config.yml index a7c167e..801c9d5 100644 --- a/config.yml +++ b/config.yml @@ -23,6 +23,9 @@ endpoints: - name: "GitHub" address: https://github.com/healthz + tls_configuration: + insecure_skip_verify: true + - name: "Reinaldy" address: https://code.reinaldyrafli.com timeout: 60 diff --git a/exporter.go b/exporter.go index 698af71..5c4df16 100644 --- a/exporter.go +++ b/exporter.go @@ -24,6 +24,7 @@ package main import ( "context" + "crypto/tls" "net/http" "strconv" "sync" @@ -54,7 +55,7 @@ var ( ) type Exporter struct { - Endpoints []Endpoint + Endpoints []PreprocessedEndpoint Logger *onelog.Logger } @@ -106,7 +107,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { wg.Add(len(e.Endpoints)) for _, endpoint := range e.Endpoints { - go func(endpoint Endpoint) { + go func(endpoint PreprocessedEndpoint) { defer wg.Done() // Create HTTP calls @@ -118,6 +119,13 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { httpClient := http.Client{ Timeout: time.Duration(uint64(endpoint.Timeout) * 1e+9), + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: endpoint.TLSConfiguration.Certificates, + RootCAs: endpoint.TLSConfiguration.RootCA, + InsecureSkipVerify: endpoint.TLSConfiguration.InsecureSkipVerify, + }, + }, } response, err := httpClient.Do(request) diff --git a/main.go b/main.go index 8a401c7..0a32393 100644 --- a/main.go +++ b/main.go @@ -49,14 +49,21 @@ func main() { var config Configuration err := configor.Load(&config, *configFile) if err != nil { - log.Fatalln(err) + log.Fatalln(err.Error()) } + preprocessedConfig, err := processConfiguration(config.Endpoints) + if err != nil { + log.Fatalln(err.Error()) + } + + log.Printf("%+v", preprocessedConfig) + // Initiate the logger instance. It should be zero-allocated, so we'll use onelog logger := onelog.New(os.Stdout, onelog.ALL) exporter := &Exporter{ - Endpoints: config.Endpoints, + Endpoints: preprocessedConfig, Logger: logger, }