master
go 171 lines 4.94 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package web
4
5 import (
6 "encoding/base64"
7 "fmt"
8 "io"
9 "maps"
10 "net/http"
11 "net/url"
12 "os"
13 "strings"
14
15 "github.com/netdata/netdata/go/plugins/pkg/buildinfo"
16 "github.com/netdata/netdata/go/plugins/pkg/executable"
17 "github.com/netdata/netdata/go/plugins/pkg/hostinfo"
18 )
19
20 // RequestConfig is the configuration of the HTTP request.
21 // This structure is not intended to be used directly as part of a module's configuration.
22 // Supported configuration file formats: YAML.
23 type RequestConfig struct {
24 // URL specifies the URL to access.
25 URL string `yaml:"url" json:"url"`
26
27 // Username specifies the username for basic HTTPConfig authentication.
28 Username string `yaml:"username,omitempty" json:"username"`
29
30 // Password specifies the password for basic HTTPConfig authentication.
31 Password string `yaml:"password,omitempty" json:"password"`
32
33 // BearerTokenFile specifies the path to a file containing a bearer token
34 // to be used for HTTP authentication.
35 // The token is read from the file and included in the Authorization header as "Bearer <token>".
36 BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file"`
37
38 // ProxyUsername specifies the username for basic HTTPConfig authentication.
39 // It is used to authenticate a user agent to a proxy server.
40 ProxyUsername string `yaml:"proxy_username,omitempty" json:"proxy_username"`
41
42 // ProxyPassword specifies the password for basic HTTPConfig authentication.
43 // It is used to authenticate a user agent to a proxy server.
44 ProxyPassword string `yaml:"proxy_password,omitempty" json:"proxy_password"`
45
46 // Method specifies the HTTPConfig method (GET, POST, PUT, etc.). An empty string means GET.
47 Method string `yaml:"method,omitempty" json:"method"`
48
49 // Headers specifies the HTTP request header fields to be sent by the client.
50 Headers map[string]string `yaml:"headers,omitempty" json:"headers"`
51
52 // Body specifies the HTTP request body to be sent by the client.
53 Body string `yaml:"body,omitempty" json:"body"`
54 }
55
56 // Copy makes a full copy of the RequestConfig.
57 func (r RequestConfig) Copy() RequestConfig {
58 if r.Headers == nil {
59 return r
60 }
61
62 headers := make(map[string]string, len(r.Headers))
63 maps.Copy(headers, r.Headers)
64 r.Headers = headers
65 return r
66 }
67
68 var userAgent = fmt.Sprintf("Netdata %s.plugin/%s", executable.Name, buildinfo.Version)
69
70 // NewHTTPRequest returns a new *http.Request given a RequestConfig configuration and an error if any.
71 func NewHTTPRequest(cfg RequestConfig) (*http.Request, error) {
72 var body io.Reader
73 if cfg.Body != "" {
74 body = strings.NewReader(cfg.Body)
75 }
76
77 method := cfg.Method
78 if method == "" {
79 method = http.MethodGet
80 }
81
82 req, err := http.NewRequest(method, cfg.URL, body)
83 if err != nil {
84 return nil, err
85 }
86
87 req.Header.Set("User-Agent", userAgent)
88
89 if err := setAuthentication(req, cfg); err != nil {
90 return nil, err
91 }
92
93 if cfg.ProxyUsername != "" && cfg.ProxyPassword != "" {
94 basicAuth := base64.StdEncoding.EncodeToString([]byte(cfg.ProxyUsername + ":" + cfg.ProxyPassword))
95 req.Header.Set("Proxy-Authorization", "Basic "+basicAuth)
96 }
97
98 for k, v := range cfg.Headers {
99 switch strings.ToLower(k) {
100 case "host":
101 req.Host = v
102 default:
103 req.Header.Set(k, v)
104 }
105 }
106
107 return req, nil
108 }
109
110 func setAuthentication(req *http.Request, cfg RequestConfig) error {
111 // Priority: Bearer Token > Basic Auth
112 switch {
113 case cfg.BearerTokenFile != "":
114 return setBearerTokenAuth(req, cfg.BearerTokenFile)
115 case cfg.Username != "" || cfg.Password != "":
116 req.SetBasicAuth(cfg.Username, cfg.Password)
117 }
118 return nil
119 }
120
121 func setBearerTokenAuth(req *http.Request, tokenFile string) error {
122 tokenBs, err := os.ReadFile(tokenFile)
123 if err != nil {
124 // Ignore K8s service account token errors when running outside the cluster
125 if strings.HasPrefix(tokenFile, "/var/run/secrets/") && !hostinfo.IsInsideK8sCluster() {
126 return nil
127 }
128 return fmt.Errorf("bearer token file: %w", err)
129 }
130
131 token := strings.TrimSpace(string(tokenBs))
132 if token == "" {
133 return fmt.Errorf("bearer token file is empty")
134 }
135
136 req.Header.Set("Authorization", "Bearer "+token)
137 return nil
138 }
139
140 // NewHTTPRequestWithPath creates a new HTTP request with the given path appended to the base URL.
141 func NewHTTPRequestWithPath(cfg RequestConfig, urlPath string) (*http.Request, error) {
142 // Make a copy to avoid modifying the original config
143 cfg = cfg.Copy()
144
145 // Join the paths properly
146 v, err := url.JoinPath(cfg.URL, urlPath)
147 if err != nil {
148 return nil, fmt.Errorf("failed to join URL path: %w", err)
149 }
150 cfg.URL = v
151
152 return NewHTTPRequest(cfg)
153 }
154
155 // URLQuery creates a URL-encoded query string from a single key-value pair.
156 func URLQuery(key, value string) string {
157 return url.Values{key: []string{value}}.Encode()
158 }
159
160 // URLQueryMulti creates a URL-encoded query string from multiple key-value pairs.
161 func URLQueryMulti(params map[string]string) string {
162 if len(params) == 0 {
163 return ""
164 }
165
166 values := url.Values{}
167 for k, v := range params {
168 values.Set(k, v)
169 }
170 return values.Encode()
171 }