File: gotify.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.8.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,332 kB
  • sloc: sh: 61; makefile: 5
file content (148 lines) | stat: -rw-r--r-- 3,805 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
package gotify

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

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

const (
	// HTTPTimeout defines the HTTP client timeout in seconds.
	HTTPTimeout = 10
	TokenLength = 15
	// TokenChars specifies the valid characters for a Gotify token.
	TokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_"
)

// ErrInvalidToken indicates an invalid Gotify token format or content.
var ErrInvalidToken = errors.New("invalid gotify token")

// Service implements a Gotify notification service.
type Service struct {
	standard.Standard
	Config     *Config
	pkr        format.PropKeyResolver
	httpClient *http.Client
	client     jsonclient.Client
}

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

	err := service.Config.SetURL(configURL)
	if err != nil {
		return err
	}

	service.httpClient = &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				// InsecureSkipVerify disables TLS certificate verification when true.
				// This is set to Config.DisableTLS to support HTTP or self-signed certificate setups,
				// but it reduces security by allowing potential man-in-the-middle attacks.
				InsecureSkipVerify: service.Config.DisableTLS,
			},
		},
		Timeout: HTTPTimeout * time.Second,
	}
	if service.Config.DisableTLS {
		service.Log("Warning: TLS verification is disabled, making connections insecure")
	}

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

	return nil
}

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

// isTokenValid checks if a Gotify token meets length and character requirements.
// Rules are based on Gotify's token validation logic.
func isTokenValid(token string) bool {
	if len(token) != TokenLength || token[0] != 'A' {
		return false
	}

	for _, c := range token {
		if !strings.ContainsRune(TokenChars, c) {
			return false
		}
	}

	return true
}

// buildURL constructs the Gotify API URL with scheme, host, path, and token.
func buildURL(config *Config) (string, error) {
	token := config.Token
	if !isTokenValid(token) {
		return "", fmt.Errorf("%w: %q", ErrInvalidToken, token)
	}

	scheme := "https"
	if config.DisableTLS {
		scheme = "http" // Use HTTP if TLS is disabled
	}

	return fmt.Sprintf("%s://%s%s/message?token=%s", scheme, config.Host, config.Path, token), nil
}

// Send delivers a notification message to Gotify.
func (service *Service) Send(message string, params *types.Params) error {
	if params == nil {
		params = &types.Params{}
	}

	config := service.Config
	if err := service.pkr.UpdateConfigFromParams(config, params); err != nil {
		service.Logf("Failed to update params: %v", err)
	}

	postURL, err := buildURL(config)
	if err != nil {
		return err
	}

	request := &messageRequest{
		Message:  message,
		Title:    config.Title,
		Priority: config.Priority,
	}
	response := &messageResponse{}

	err = service.client.Post(postURL, request, response)
	if err != nil {
		errorRes := &responseError{}
		if service.client.ErrorResponse(err, errorRes) {
			return errorRes
		}

		return fmt.Errorf("failed to send notification to Gotify: %w", err)
	}

	return nil
}

// GetHTTPClient returns the HTTP client for testing purposes.
func (service *Service) GetHTTPClient() *http.Client {
	return service.httpClient
}