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)
}
}
|