File: ntfy.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.12.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,680 kB
  • sloc: sh: 74; makefile: 58
file content (156 lines) | stat: -rw-r--r-- 4,686 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package ntfy

import (
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/nicholas-fedor/shoutrrr/internal/meta"
	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
	"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)

// HTTPTimeout defines the HTTP client timeout in seconds.
const HTTPTimeout = 10

// Service sends notifications to ntfy.
type Service struct {
	standard.Standard
	Config     *Config
	pkr        format.PropKeyResolver
	httpClient *http.Client
	client     jsonclient.Client
}

// SetHTTPClient sets a custom HTTP client for the service.
func (service *Service) SetHTTPClient(httpClient *http.Client) {
	service.httpClient = httpClient
	service.client = jsonclient.NewWithHTTPClient(service.httpClient)
}

// Send delivers a notification message to ntfy.
func (service *Service) Send(message string, params *types.Params) error {
	config := service.Config

	// Update config with runtime parameters
	if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
		return fmt.Errorf("updating config from params: %w", err)
	}

	// Execute the API request to send the notification
	if err := service.sendAPI(config, message); err != nil {
		return fmt.Errorf("failed to send ntfy notification: %w", err)
	}

	return nil
}

// Initialize configures the service with a URL and logger.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
	service.SetLogger(logger)
	service.Config = &Config{}
	service.pkr = format.NewPropKeyResolver(service.Config)

	_ = service.pkr.SetDefaultProps(service.Config)

	err := service.Config.setURL(&service.pkr, configURL)
	if err != nil {
		return err
	}

	service.httpClient = &http.Client{
		Timeout: HTTPTimeout * time.Second,
	}

	// Configure HTTP transport for TLS verification and enforce TLS 1.2
	if service.Config.DisableTLSVerification {
		service.httpClient.Transport = &http.Transport{
			//nolint:gosec // TLS verification intentionally disabled
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
				MinVersion:         tls.VersionTLS12,
			},
		}
		service.Log("Warning: TLS verification is disabled, making connections insecure")
	} else {
		service.httpClient.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
		}
		service.Log("Using custom HTTP transport with TLS verification enabled and TLS 1.2 enforced")
	}

	service.client = jsonclient.NewWithHTTPClient(service.httpClient)

	return nil
}

// GetID returns the service identifier.
func (service *Service) GetID() string {
	return Scheme
}

// sendAPI sends a notification to the ntfy API.
func (service *Service) sendAPI(config *Config, message string) error {
	response := apiResponse{}
	request := message

	// Prepare request headers
	headers := service.client.Headers()
	headers.Del("Content-Type")
	headers.Set("Content-Type", "text/plain; charset=utf-8")
	headers.Set("User-Agent", "shoutrrr/"+meta.Version)
	addHeaderIfNotEmpty(&headers, "Title", config.Title)
	addHeaderIfNotEmpty(&headers, "Priority", config.Priority.String())
	addHeaderIfNotEmpty(&headers, "Tags", strings.Join(config.Tags, ","))
	addHeaderIfNotEmpty(&headers, "Delay", config.Delay)
	addHeaderIfNotEmpty(&headers, "Actions", strings.Join(config.Actions, ";"))
	addHeaderIfNotEmpty(&headers, "Click", config.Click)
	addHeaderIfNotEmpty(&headers, "Attach", config.Attach)
	addHeaderIfNotEmpty(&headers, "X-Icon", config.Icon)
	addHeaderIfNotEmpty(&headers, "Filename", config.Filename)
	addHeaderIfNotEmpty(&headers, "Email", config.Email)

	if !config.Cache {
		headers.Add("Cache", "no")
	}

	if !config.Firebase {
		headers.Add("Firebase", "no")
	}

	// Add Basic Auth header if username or password is provided
	if config.Username != "" || config.Password != "" {
		headers.Set(
			"Authorization",
			"Basic "+base64.StdEncoding.EncodeToString([]byte(config.Username+":"+config.Password)),
		)
	}

	// Send the HTTP request
	if err := service.client.Post(config.GetAPIURL(), request, &response); err != nil {
		service.Logf("NTFY API request failed with error: %v", err)
		// Attempt to parse structured error response from API
		if service.client.ErrorResponse(err, &response) {
			return &response
		}

		return fmt.Errorf("posting to ntfy API: %w", err)
	}

	service.Logf("NTFY API request succeeded")

	return nil
}

// addHeaderIfNotEmpty adds a header to the request if the value is non-empty.
func addHeaderIfNotEmpty(headers *http.Header, key string, value string) {
	if value != "" {
		headers.Add(key, value)
	}
}