From 288d3395c350f61726f7110b60800afd56725417 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:31:43 +0200 Subject: [PATCH] Add TLS config options for notifiers using HTTP client --- docs/faq.md | 37 ++++++++++++++++++++ docs/notif/apprise.md | 24 +++++++------ docs/notif/elasticsearch.md | 27 +++++++------- docs/notif/gotify.md | 4 +++ docs/notif/ntfy.md | 4 +++ docs/notif/rocketchat.md | 4 +++ docs/notif/signalrest.md | 4 +++ docs/notif/teams.md | 6 ++++ docs/notif/webhook.md | 16 +++++---- internal/config/config_test.go | 14 ++++---- internal/config/fixtures/config.test.yml | 1 - internal/config/fixtures/config.validate.yml | 1 - internal/model/notif_apprise.go | 18 +++++----- internal/model/notif_elasticsearch.go | 20 +++++------ internal/model/notif_gotify.go | 16 +++++---- internal/model/notif_ntfy.go | 20 ++++++----- internal/model/notif_rocketchat.go | 2 ++ internal/model/notif_signalrest.go | 14 ++++---- internal/model/notif_teams.go | 18 +++++++--- internal/model/notif_webhook.go | 10 +++--- internal/notif/apprise/client.go | 10 +++++- internal/notif/elasticsearch/client.go | 9 ++--- internal/notif/gotify/client.go | 10 +++++- internal/notif/ntfy/client.go | 11 +++++- internal/notif/rocketchat/client.go | 11 +++++- internal/notif/signalrest/client.go | 12 ++++++- internal/notif/teams/client.go | 13 +++++-- internal/notif/webhook/client.go | 12 ++++++- pkg/utl/http.go | 25 +++++++++++++ 29 files changed, 274 insertions(+), 99 deletions(-) create mode 100644 pkg/utl/http.go diff --git a/docs/faq.md b/docs/faq.md index 332bfbb3..9f63273a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -296,6 +296,43 @@ And for `semver`: ] ``` +## Custom CA certificates for notification endpoints + +If your notification endpoint (e.g. Gotify, Ntfy, Telegram, Webhook, etc.) is +using a self-signed certificate or a certificate issued by a private CA, you +can provide the CA certificate to Diun through the `tlsCaCertFiles` setting: + +```yaml +notif: + gotify: + endpoint: https://gotify.foo.com + token: Token123456 + tlsCaCertFiles: + - /certs/ca-gotify.crt +``` + +Then mount the certificate file in the container: + +```yaml +name: diun + +services: + diun: + image: crazymax/diun:latest + container_name: diun + command: serve + volumes: + - "./data:/data" + - "/etc/ssl/certs/ca-gotify.crt:/certs/ca-gotify.crt:ro" + - "/var/run/docker.sock:/var/run/docker.sock" + environment: + - "TZ=Europe/Paris" + - "DIUN_WATCH_SCHEDULE=0 */6 * * *" + - "DIUN_PROVIDERS_DOCKER=true" + - "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true" + restart: always +``` + ## Profiling Diun provides a simple way to manage runtime/pprof profiling through the diff --git a/docs/notif/apprise.md b/docs/notif/apprise.md index bbe1eb88..33389d94 100644 --- a/docs/notif/apprise.md +++ b/docs/notif/apprise.md @@ -18,16 +18,18 @@ Notifications can be sent using an apprise api instance. Docker tag {{ .Entry.Image }} which you subscribed to through {{ .Entry.Provider }} provider has been released. ``` -| Name | Default | Description | -|-----------------|-------------------------------------|----------------------------------------------------------------------------| -| `endpoint`[^1] | | Hostname and port of your apprise api instance | -| `token`[^2] | | token representing your config file (Config Key) | -| `tokenFile` | | Use content of secret file as application token if `token` not defined | -| `tags` | | List of Tags in your config file you want to notify | -| `urls`[^2] | | List of [URLs](https://github.com/caronc/apprise/wiki/URLBasics) to notify | -| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | -| `templateTitle` | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | -| `templateBody` | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | +| Name | Default | Description | +|------------------|-------------------------------------|----------------------------------------------------------------------------| +| `endpoint`[^1] | | Hostname and port of your apprise api instance | +| `token`[^2] | | token representing your config file (Config Key) | +| `tokenFile` | | Use content of secret file as application token if `token` not defined | +| `tags` | | List of Tags in your config file you want to notify | +| `urls`[^2] | | List of [URLs](https://github.com/caronc/apprise/wiki/URLBasics) to notify | +| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | +| `templateTitle` | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | +| `templateBody` | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | !!! abstract "Environment variables" * `DIUN_NOTIF_APPRISE_ENDPOINT` @@ -35,6 +37,8 @@ Notifications can be sent using an apprise api instance. * `DIUN_NOTIF_APPRISE_TAGS` * `DIUN_NOTIF_APPRISE_URLS` * `DIUN_NOTIF_APPRISE_TIMEOUT` + * `DIUN_NOTIF_APPRISE_TLSSKIPVERIFY` + * `DIUN_NOTIF_APPRISE_TLSCACERTFILES` * `DIUN_NOTIF_APPRISE_TEMPLATETITLE` * `DIUN_NOTIF_APPRISE_TEMPLATEBODY` diff --git a/docs/notif/elasticsearch.md b/docs/notif/elasticsearch.md index c7884f1a..12a37029 100644 --- a/docs/notif/elasticsearch.md +++ b/docs/notif/elasticsearch.md @@ -14,20 +14,20 @@ Send notifications to your Elasticsearch cluster as structured documents. client: diun index: diun-notifications timeout: 10s - insecureSkipVerify: false ``` -| Name | Default | Description | -|----------------------|-------------------------|---------------------------------------------------------------------| -| `address`[^1] | `http://localhost:9200` | Elasticsearch base URL | -| `username` | | Elasticsearch username for authentication | -| `usernameFile` | | Use content of secret file as username if `username` is not defined | -| `password` | | Elasticsearch password for authentication | -| `passwordFile` | | Use content of secret file as password if `password` is not defined | -| `client`[^1] | `diun` | Client name to identify the source of notifications | -| `index`[^1] | `diun-notifications` | Elasticsearch index name where notifications will be stored | -| `timeout`[^1] | `10s` | Timeout specifies a time limit for the request to be made | -| `insecureSkipVerify` | `false` | Skip TLS certificate verification | +| Name | Default | Description | +|------------------|-------------------------|--------------------------------------------------------------------------| +| `address`[^1] | `http://localhost:9200` | Elasticsearch base URL | +| `username` | | Elasticsearch username for authentication | +| `usernameFile` | | Use content of secret file as username if `username` is not defined | +| `password` | | Elasticsearch password for authentication | +| `passwordFile` | | Use content of secret file as password if `password` is not defined | +| `client`[^1] | `diun` | Client name to identify the source of notifications | +| `index`[^1] | `diun-notifications` | Elasticsearch index name where notifications will be stored | +| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | !!! abstract "Environment variables" * `DIUN_NOTIF_ELASTICSEARCH_ADDRESS` @@ -38,7 +38,8 @@ Send notifications to your Elasticsearch cluster as structured documents. * `DIUN_NOTIF_ELASTICSEARCH_CLIENT` * `DIUN_NOTIF_ELASTICSEARCH_INDEX` * `DIUN_NOTIF_ELASTICSEARCH_TIMEOUT` - * `DIUN_NOTIF_ELASTICSEARCH_INSECURESKIPVERIFY` + * `DIUN_NOTIF_ELASTICSEARCH_TLSSKIPVERIFY` + * `DIUN_NOTIF_ELASTICSEARCH_TLSCACERTFILES` ## Document Structure diff --git a/docs/notif/gotify.md b/docs/notif/gotify.md index 31cc1e88..f878c591 100644 --- a/docs/notif/gotify.md +++ b/docs/notif/gotify.md @@ -24,6 +24,8 @@ Notifications can be sent using a [Gotify](https://gotify.net/) instance. | `tokenFile` | | Use content of secret file as application token if `token` not defined | | `priority` | `1` | The priority of the message | | `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | | `templateTitle`[^1] | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | @@ -33,6 +35,8 @@ Notifications can be sent using a [Gotify](https://gotify.net/) instance. * `DIUN_NOTIF_GOTIFY_TOKENFILE` * `DIUN_NOTIF_GOTIFY_PRIORITY` * `DIUN_NOTIF_GOTIFY_TIMEOUT` + * `DIUN_NOTIF_GOTIFY_TLSSKIPVERIFY` + * `DIUN_NOTIF_GOTIFY_TLSCACERTFILES` * `DIUN_NOTIF_GOTIFY_TEMPLATETITLE` * `DIUN_NOTIF_GOTIFY_TEMPLATEBODY` diff --git a/docs/notif/ntfy.md b/docs/notif/ntfy.md index 6c9832cf..e1abda6e 100644 --- a/docs/notif/ntfy.md +++ b/docs/notif/ntfy.md @@ -28,6 +28,8 @@ Notifications can be sent using a [ntfy](https://ntfy.sh/) instance. | `priority` | 3 | The priority of the message | | `tags` | `["package"]` | Emoji to go in your notiication | | `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | | `templateTitle`[^1] | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | @@ -39,6 +41,8 @@ Notifications can be sent using a [ntfy](https://ntfy.sh/) instance. * `DIUN_NOTIF_NTFY_PRIORITY` * `DIUN_NOTIF_NTFY_TAGS` * `DIUN_NOTIF_NTFY_TIMEOUT` + * `DIUN_NOTIF_NTFY_TLSSKIPVERIFY` + * `DIUN_NOTIF_NTFY_TLSCACERTFILES` * `DIUN_NOTIF_NTFY_TEMPLATETITLE` * `DIUN_NOTIF_NTFY_TEMPLATEBODY` diff --git a/docs/notif/rocketchat.md b/docs/notif/rocketchat.md index 5d129d7b..faa85b30 100644 --- a/docs/notif/rocketchat.md +++ b/docs/notif/rocketchat.md @@ -28,6 +28,8 @@ Allow sending notifications to your Rocket.Chat channel. | `tokenFile` | | Use content of secret file as authentication token if `token` not defined | | `renderAttachment` | `true` | Render [attachment object](https://docs.rocket.chat/guides/user-guides/messaging#send-attachments) | | `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | | `templateTitle`[^1] | See [below](#default-templatetitle) | [Notification template](../faq.md#notification-template) for message title | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | @@ -44,6 +46,8 @@ Allow sending notifications to your Rocket.Chat channel. * `DIUN_NOTIF_ROCKETCHAT_TOKENFILE` * `DIUN_NOTIF_ROCKETCHAT_RENDERATTACHMENT` * `DIUN_NOTIF_ROCKETCHAT_TIMEOUT` + * `DIUN_NOTIF_ROCKETCHAT_TLSSKIPVERIFY` + * `DIUN_NOTIF_ROCKETCHAT_TLSCACERTFILES` * `DIUN_NOTIF_ROCKETCHAT_TEMPLATETITLE` * `DIUN_NOTIF_ROCKETCHAT_TEMPLATEBODY` diff --git a/docs/notif/signalrest.md b/docs/notif/signalrest.md index db67bbdd..dda53e78 100644 --- a/docs/notif/signalrest.md +++ b/docs/notif/signalrest.md @@ -25,12 +25,16 @@ You can send Signal notifications via the Signal REST API with the following set | `number`[^1] | | The senders number you registered | | `recipients`[^1] | | A list of recipients, either phone numbers or group ID's | | `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | !!! abstract "Environment variables" * `DIUN_NOTIF_SIGNALREST_ENDPOINT` * `DIUN_NOTIF_SIGNALREST_NUMBER` * `DIUN_NOTIF_SIGNALREST_RECIPIENTS_` + * `DIUN_NOTIF_SIGNALREST_TLSSKIPVERIFY` + * `DIUN_NOTIF_SIGNALREST_TLSCACERTFILES` * `DIUN_NOTIF_SIGNALREST_TIMEOUT` ### Default `templateBody` diff --git a/docs/notif/teams.md b/docs/notif/teams.md index ae6044b7..1c4a7ef8 100644 --- a/docs/notif/teams.md +++ b/docs/notif/teams.md @@ -19,12 +19,18 @@ You can send notifications to your Teams team-channel using an [incoming webhook | `webhookURL` | | Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) | | `webhookURLFile` | | Use content of secret file as webhook URL if `webhookURL` is not defined | | `renderFacts` | `true` | Render fact objects | +| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | !!! abstract "Environment variables" * `DIUN_NOTIF_TEAMS_WEBHOOKURL` * `DIUN_NOTIF_TEAMS_WEBHOOKURLFILE` * `DIUN_NOTIF_TEAMS_RENDERFACTS` + * `DIUN_NOTIF_TEAMS_TIMEOUT` + * `DIUN_NOTIF_TEAMS_TLSSKIPVERIFY` + * `DIUN_NOTIF_TEAMS_TLSCACERTFILES` * `DIUN_NOTIF_TEAMS_TEMPLATEBODY` ### Default `templateBody` diff --git a/docs/notif/webhook.md b/docs/notif/webhook.md index fe27ef10..b5daa549 100644 --- a/docs/notif/webhook.md +++ b/docs/notif/webhook.md @@ -16,18 +16,22 @@ You can send webhook notifications with the following settings. timeout: 10s ``` -| Name | Default | Description | -|----------------|---------|----------------------------------------------------------------| -| `endpoint`[^1] | | URL of the HTTP request | -| `method`[^1] | `GET` | HTTP method | -| `headers` | | Map of additional headers to be sent (key is case insensitive) | -| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| Name | Default | Description | +|------------------|---------|--------------------------------------------------------------------------| +| `endpoint`[^1] | | URL of the HTTP request | +| `method`[^1] | `GET` | HTTP method | +| `headers` | | Map of additional headers to be sent (key is case insensitive) | +| `timeout` | `10s` | Timeout specifies a time limit for the request to be made | +| `tlsSkipVerify` | `false` | Skip TLS certificate verification | +| `tlsCaCertFiles` | | List of paths to custom CA certificate files to use for TLS verification | !!! abstract "Environment variables" * `DIUN_NOTIF_WEBHOOK_ENDPOINT` * `DIUN_NOTIF_WEBHOOK_METHOD` * `DIUN_NOTIF_WEBHOOK_HEADERS_` * `DIUN_NOTIF_WEBHOOK_TIMEOUT` + * `DIUN_NOTIF_WEBHOOK_TLSSKIPVERIFY` + * `DIUN_NOTIF_WEBHOOK_TLSCACERTFILES` ## Sample diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c31a6861..1a282697 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -95,13 +95,12 @@ func TestLoadFile(t *testing.T) { TemplateBody: model.NotifDefaultTemplateBody, }, Elasticsearch: &model.NotifElasticsearch{ - Address: "https://elastic.foo.com", - Username: "elastic", - Password: "password", - Client: "diun", - Index: "diun-notifications", - Timeout: utl.NewDuration(10 * time.Second), - InsecureSkipVerify: false, + Address: "https://elastic.foo.com", + Username: "elastic", + Password: "password", + Client: "diun", + Index: "diun-notifications", + Timeout: utl.NewDuration(10 * time.Second), }, Gotify: &model.NotifGotify{ Endpoint: "http://gotify.foo.com", @@ -188,6 +187,7 @@ for {{ .Entry.Manifest.Platform }} platform. Teams: &model.NotifTeams{ WebhookURL: "https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij", RenderFacts: utl.NewFalse(), + Timeout: utl.NewDuration(10 * time.Second), TemplateBody: model.NotifTeamsDefaultTemplateBody, }, Telegram: &model.NotifTelegram{ diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml index bb305594..8aac8c13 100644 --- a/internal/config/fixtures/config.test.yml +++ b/internal/config/fixtures/config.test.yml @@ -46,7 +46,6 @@ notif: client: diun index: diun-notifications timeout: 10s - insecureSkipVerify: false gotify: endpoint: http://gotify.foo.com token: Token123456 diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml index 7a6f3efa..76301227 100644 --- a/internal/config/fixtures/config.validate.yml +++ b/internal/config/fixtures/config.validate.yml @@ -35,7 +35,6 @@ notif: client: diun index: diun-notifications timeout: 10s - insecureSkipVerify: false gotify: endpoint: http://gotify.foo.com token: Token123456 diff --git a/internal/model/notif_apprise.go b/internal/model/notif_apprise.go index de3649f1..3e7335df 100644 --- a/internal/model/notif_apprise.go +++ b/internal/model/notif_apprise.go @@ -8,14 +8,16 @@ import ( // NotifApprise holds apprise notification configuration details type NotifApprise struct { - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` - Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` - TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` - Tags []string `yaml:"tags,omitempty" json:"tags,omitempty" validate:"omitempty"` - URLs []string `yaml:"urls,omitempty" json:"urls,omitempty" validate:"omitempty"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` - TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` + Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` + TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` + Tags []string `yaml:"tags,omitempty" json:"tags,omitempty" validate:"omitempty"` + URLs []string `yaml:"urls,omitempty" json:"urls,omitempty" validate:"omitempty"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` + TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values diff --git a/internal/model/notif_elasticsearch.go b/internal/model/notif_elasticsearch.go index 3df60779..6359beff 100644 --- a/internal/model/notif_elasticsearch.go +++ b/internal/model/notif_elasticsearch.go @@ -7,15 +7,16 @@ import ( ) type NotifElasticsearch struct { - Address string `yaml:"address,omitempty" json:"address,omitempty" validate:"required"` - Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"` - UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"` - Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"` - PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"` - Client string `yaml:"client,omitempty" json:"client,omitempty" validate:"required"` - Index string `yaml:"index,omitempty" json:"index,omitempty" validate:"required"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` - InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify,omitempty" validate:"omitempty"` + Address string `yaml:"address,omitempty" json:"address,omitempty" validate:"required"` + Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"` + UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"` + Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"` + PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"` + Client string `yaml:"client,omitempty" json:"client,omitempty" validate:"required"` + Index string `yaml:"index,omitempty" json:"index,omitempty" validate:"required"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` } // GetDefaults gets the default values @@ -31,5 +32,4 @@ func (s *NotifElasticsearch) SetDefaults() { s.Client = "diun" s.Index = "diun-notifications" s.Timeout = utl.NewDuration(10 * time.Second) - s.InsecureSkipVerify = false } diff --git a/internal/model/notif_gotify.go b/internal/model/notif_gotify.go index 19bc7481..e0ba3173 100644 --- a/internal/model/notif_gotify.go +++ b/internal/model/notif_gotify.go @@ -8,13 +8,15 @@ import ( // NotifGotify holds gotify notification configuration details type NotifGotify struct { - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` - Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` - TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` - Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=0"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` - TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` + Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` + TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` + Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=0"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` + TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values diff --git a/internal/model/notif_ntfy.go b/internal/model/notif_ntfy.go index f2092703..a8e17e1b 100644 --- a/internal/model/notif_ntfy.go +++ b/internal/model/notif_ntfy.go @@ -8,15 +8,17 @@ import ( // NotifNtfy holds ntfy notification configuration details type NotifNtfy struct { - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` - Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` - TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` - Topic string `yaml:"topic,omitempty" json:"topic,omitempty" validate:"required"` - Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=0"` - Tags []string `yaml:"tags,omitempty" json:"tags,omitempty" validate:"required"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` - TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` + Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` + TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` + Topic string `yaml:"topic,omitempty" json:"topic,omitempty" validate:"required"` + Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=0"` + Tags []string `yaml:"tags,omitempty" json:"tags,omitempty" validate:"required"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` + TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values diff --git a/internal/model/notif_rocketchat.go b/internal/model/notif_rocketchat.go index a82d0a51..f347b564 100644 --- a/internal/model/notif_rocketchat.go +++ b/internal/model/notif_rocketchat.go @@ -18,6 +18,8 @@ type NotifRocketChat struct { TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` RenderAttachment *bool `yaml:"renderAttachment,omitempty" json:"renderAttachment,omitempty" validate:"required"` Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` TemplateTitle string `yaml:"templateTitle,omitempty" json:"templateTitle,omitempty" validate:"required"` TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } diff --git a/internal/model/notif_signalrest.go b/internal/model/notif_signalrest.go index c7ac5f85..66a180fc 100644 --- a/internal/model/notif_signalrest.go +++ b/internal/model/notif_signalrest.go @@ -11,12 +11,14 @@ const NotifSignalRestDefaultTemplateBody = `Docker tag {{ .Entry.Image }} which // NotifSignalRest holds SignalRest notification configuration details type NotifSignalRest struct { - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` - Number string `yaml:"number,omitempty" json:"method,omitempty" validate:"required"` - Recipients []string `yaml:"recipients,omitempty" json:"recipients,omitempty" validate:"omitempty"` - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` + Number string `yaml:"number,omitempty" json:"method,omitempty" validate:"required"` + Recipients []string `yaml:"recipients,omitempty" json:"recipients,omitempty" validate:"omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values diff --git a/internal/model/notif_teams.go b/internal/model/notif_teams.go index 792f5528..1afd03a9 100644 --- a/internal/model/notif_teams.go +++ b/internal/model/notif_teams.go @@ -1,16 +1,23 @@ package model -import "github.com/crazy-max/diun/v4/pkg/utl" +import ( + "time" + + "github.com/crazy-max/diun/v4/pkg/utl" +) // NotifTeamsDefaultTemplateBody ... const NotifTeamsDefaultTemplateBody = "Docker tag {{ if .Entry.Image.HubLink }}[`{{ .Entry.Image }}`]({{ .Entry.Image.HubLink }}){{ else }}`{{ .Entry.Image }}`{{ end }} {{ if (eq .Entry.Status \"new\") }}available{{ else }}updated{{ end }}." // NotifTeams holds Teams notification configuration details type NotifTeams struct { - WebhookURL string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"omitempty"` - WebhookURLFile string `yaml:"webhookURLFile,omitempty" json:"webhookURLFile,omitempty" validate:"omitempty,file"` - RenderFacts *bool `yaml:"renderFacts,omitempty" json:"renderFacts,omitempty" validate:"required"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + WebhookURL string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"omitempty"` + WebhookURLFile string `yaml:"webhookURLFile,omitempty" json:"webhookURLFile,omitempty" validate:"omitempty,file"` + RenderFacts *bool `yaml:"renderFacts,omitempty" json:"renderFacts,omitempty" validate:"required"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values @@ -22,6 +29,7 @@ func (s *NotifTeams) GetDefaults() *NotifTeams { // SetDefaults sets the default values func (s *NotifTeams) SetDefaults() { + s.Timeout = utl.NewDuration(10 * time.Second) s.RenderFacts = utl.NewTrue() s.TemplateBody = NotifTeamsDefaultTemplateBody } diff --git a/internal/model/notif_webhook.go b/internal/model/notif_webhook.go index 57fca452..7f64a5e8 100644 --- a/internal/model/notif_webhook.go +++ b/internal/model/notif_webhook.go @@ -8,10 +8,12 @@ import ( // NotifWebhook holds webhook notification configuration details type NotifWebhook struct { - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` - Method string `yaml:"method,omitempty" json:"method,omitempty" validate:"required"` - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty"` - Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"` + Method string `yaml:"method,omitempty" json:"method,omitempty" validate:"required"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty"` + Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"` + TLSSkipVerify bool `yaml:"tlsSkipVerify,omitempty" json:"tlsSkipVerify,omitempty" validate:"omitempty"` + TLSCACertFiles []string `yaml:"tlsCaCertFiles,omitempty" json:"tlsCaCertFiles,omitempty" validate:"omitempty"` } // GetDefaults gets the default values diff --git a/internal/notif/apprise/client.go b/internal/notif/apprise/client.go index e79edcdd..a6d9bd66 100644 --- a/internal/notif/apprise/client.go +++ b/internal/notif/apprise/client.go @@ -91,7 +91,15 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Apprise notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } req, err := http.NewRequestWithContext(timeoutCtx, "POST", u.String(), dataBuf) if err != nil { return err diff --git a/internal/notif/elasticsearch/client.go b/internal/notif/elasticsearch/client.go index 1b1cff77..f2d4b4a3 100644 --- a/internal/notif/elasticsearch/client.go +++ b/internal/notif/elasticsearch/client.go @@ -3,7 +3,6 @@ package elasticsearch import ( "bytes" "context" - "crypto/tls" "encoding/json" "net/http" "net/url" @@ -96,11 +95,13 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Elasticsearch notifier") + } hc := http.Client{ Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: c.cfg.InsecureSkipVerify, - }, + TLSClientConfig: tlsConfig, }, } diff --git a/internal/notif/gotify/client.go b/internal/notif/gotify/client.go index c7c83020..6c27d674 100644 --- a/internal/notif/gotify/client.go +++ b/internal/notif/gotify/client.go @@ -93,7 +93,15 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Gotify notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } req, err := http.NewRequestWithContext(timeoutCtx, "POST", u.String(), bytes.NewBuffer(jsonBody)) if err != nil { return err diff --git a/internal/notif/ntfy/client.go b/internal/notif/ntfy/client.go index 39182dde..71878510 100644 --- a/internal/notif/ntfy/client.go +++ b/internal/notif/ntfy/client.go @@ -85,7 +85,16 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for ntfy notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + req, err := http.NewRequestWithContext(timeoutCtx, "POST", u.String(), dataBuf) if err != nil { return err diff --git a/internal/notif/rocketchat/client.go b/internal/notif/rocketchat/client.go index b0264834..7577bfe4 100644 --- a/internal/notif/rocketchat/client.go +++ b/internal/notif/rocketchat/client.go @@ -126,7 +126,16 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Rocket.Chat notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + req, err := http.NewRequestWithContext(timeoutCtx, "POST", u.String(), dataBuf) if err != nil { return err diff --git a/internal/notif/signalrest/client.go b/internal/notif/signalrest/client.go index 0da3317b..bc9dc8fe 100644 --- a/internal/notif/signalrest/client.go +++ b/internal/notif/signalrest/client.go @@ -9,6 +9,7 @@ import ( "github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/internal/msg" "github.com/crazy-max/diun/v4/internal/notif/notifier" + "github.com/crazy-max/diun/v4/pkg/utl" "github.com/pkg/errors" ) @@ -67,7 +68,16 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Signal-REST notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + req, err := http.NewRequestWithContext(timeoutCtx, "POST", c.cfg.Endpoint, bytes.NewBuffer(body)) if err != nil { return err diff --git a/internal/notif/teams/client.go b/internal/notif/teams/client.go index b1b776df..1bd565ea 100644 --- a/internal/notif/teams/client.go +++ b/internal/notif/teams/client.go @@ -111,10 +111,19 @@ func (c *Client) Send(entry model.NotifEntry) error { } cancelCtx, cancel := context.WithCancelCause(context.Background()) - timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, 10*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent + timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Teams notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + req, err := http.NewRequestWithContext(timeoutCtx, "POST", webhookURL, bytes.NewBuffer(jsonBody)) if err != nil { return err diff --git a/internal/notif/webhook/client.go b/internal/notif/webhook/client.go index 56853e80..c4a8fd38 100644 --- a/internal/notif/webhook/client.go +++ b/internal/notif/webhook/client.go @@ -8,6 +8,7 @@ import ( "github.com/crazy-max/diun/v4/internal/model" "github.com/crazy-max/diun/v4/internal/msg" "github.com/crazy-max/diun/v4/internal/notif/notifier" + "github.com/crazy-max/diun/v4/pkg/utl" "github.com/pkg/errors" ) @@ -52,7 +53,16 @@ func (c *Client) Send(entry model.NotifEntry) error { timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, *c.cfg.Timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent defer func() { cancel(errors.WithStack(context.Canceled)) }() - hc := http.Client{} + tlsConfig, err := utl.LoadTLSConfig(c.cfg.TLSSkipVerify, c.cfg.TLSCACertFiles) + if err != nil { + return errors.Wrap(err, "cannot load TLS configuration for Webhook notifier") + } + hc := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + req, err := http.NewRequestWithContext(timeoutCtx, "POST", c.cfg.Endpoint, bytes.NewBuffer(body)) if err != nil { return err diff --git a/pkg/utl/http.go b/pkg/utl/http.go new file mode 100644 index 00000000..fa816fb8 --- /dev/null +++ b/pkg/utl/http.go @@ -0,0 +1,25 @@ +package utl + +import ( + "crypto/tls" + "crypto/x509" + "os" +) + +func LoadTLSConfig(insecureSkipVerify bool, caCertFiles []string) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + if len(caCertFiles) > 0 { + certPool := x509.NewCertPool() + for _, caCertFile := range caCertFiles { + caCert, err := os.ReadFile(caCertFile) + if err != nil { + return nil, err + } + certPool.AppendCertsFromPEM(caCert) + } + tlsConfig.RootCAs = certPool + } + return tlsConfig, nil +}