cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2022.6.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

config/configuration.go

436lines · modecode

1package config
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/url"
8 "os"
9 "path/filepath"
10 "runtime"
11 "strconv"
12 "time"
13
14 homedir "github.com/mitchellh/go-homedir"
15 "github.com/pkg/errors"
16 "github.com/rs/zerolog"
17 "github.com/urfave/cli/v2"
18 yaml "gopkg.in/yaml.v3"
19
20 "github.com/cloudflare/cloudflared/validation"
21)
22
23var (
24 // DefaultConfigFiles is the file names from which we attempt to read configuration.
25 DefaultConfigFiles = []string{"config.yml", "config.yaml"}
26
27 // DefaultUnixConfigLocation is the primary location to find a config file
28 DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
29
30 // DefaultUnixLogLocation is the primary location to find log files
31 DefaultUnixLogLocation = "/var/log/cloudflared"
32
33 // Launchd doesn't set root env variables, so there is default
34 // Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
35 defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
36 defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
37
38 ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
39)
40
41const (
42 DefaultCredentialFile = "cert.pem"
43
44 // BastionFlag is to enable bastion, or jump host, operation
45 BastionFlag = "bastion"
46)
47
48// DefaultConfigDirectory returns the default directory of the config file
49func DefaultConfigDirectory() string {
50 if runtime.GOOS == "windows" {
51 path := os.Getenv("CFDPATH")
52 if path == "" {
53 path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
54 if _, err := os.Stat(path); os.IsNotExist(err) { // doesn't exist, so return an empty failure string
55 return ""
56 }
57 }
58 return path
59 }
60 return DefaultUnixConfigLocation
61}
62
63// DefaultLogDirectory returns the default directory for log files
64func DefaultLogDirectory() string {
65 if runtime.GOOS == "windows" {
66 return DefaultConfigDirectory()
67 }
68 return DefaultUnixLogLocation
69}
70
71// DefaultConfigPath returns the default location of a config file
72func DefaultConfigPath() string {
73 dir := DefaultConfigDirectory()
74 if dir == "" {
75 return DefaultConfigFiles[0]
76 }
77 return filepath.Join(dir, DefaultConfigFiles[0])
78}
79
80// DefaultConfigSearchDirectories returns the default folder locations of the config
81func DefaultConfigSearchDirectories() []string {
82 dirs := make([]string, len(defaultUserConfigDirs))
83 copy(dirs, defaultUserConfigDirs)
84 if runtime.GOOS != "windows" {
85 dirs = append(dirs, defaultNixConfigDirs...)
86 }
87 return dirs
88}
89
90// FileExists checks to see if a file exist at the provided path.
91func FileExists(path string) (bool, error) {
92 f, err := os.Open(path)
93 if err != nil {
94 if os.IsNotExist(err) {
95 // ignore missing files
96 return false, nil
97 }
98 return false, err
99 }
100 _ = f.Close()
101 return true, nil
102}
103
104// FindDefaultConfigPath returns the first path that contains a config file.
105// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
106// contains a config file, return empty string.
107func FindDefaultConfigPath() string {
108 for _, configDir := range DefaultConfigSearchDirectories() {
109 for _, configFile := range DefaultConfigFiles {
110 dirPath, err := homedir.Expand(configDir)
111 if err != nil {
112 continue
113 }
114 path := filepath.Join(dirPath, configFile)
115 if ok, _ := FileExists(path); ok {
116 return path
117 }
118 }
119 }
120 return ""
121}
122
123// FindOrCreateConfigPath returns the first path that contains a config file
124// or creates one in the primary default path if it doesn't exist
125func FindOrCreateConfigPath() string {
126 path := FindDefaultConfigPath()
127
128 if path == "" {
129 // create the default directory if it doesn't exist
130 path = DefaultConfigPath()
131 if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
132 return ""
133 }
134
135 // write a new config file out
136 file, err := os.Create(path)
137 if err != nil {
138 return ""
139 }
140 defer file.Close()
141
142 logDir := DefaultLogDirectory()
143 _ = os.MkdirAll(logDir, os.ModePerm) // try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
144
145 c := Root{
146 LogDirectory: logDir,
147 }
148 if err := yaml.NewEncoder(file).Encode(&c); err != nil {
149 return ""
150 }
151 }
152
153 return path
154}
155
156// ValidateUnixSocket ensures --unix-socket param is used exclusively
157// i.e. it fails if a user specifies both --url and --unix-socket
158func ValidateUnixSocket(c *cli.Context) (string, error) {
159 if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
160 return "", errors.New("--unix-socket must be used exclusivly.")
161 }
162 return c.String("unix-socket"), nil
163}
164
165// ValidateUrl will validate url flag correctness. It can be either from --url or argument
166// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
167func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
168 var url = c.String("url")
169 if allowURLFromArgs && c.NArg() > 0 {
170 if c.IsSet("url") {
171 return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
172 }
173 url = c.Args().Get(0)
174 }
175 validUrl, err := validation.ValidateUrl(url)
176 return validUrl, err
177}
178
179type UnvalidatedIngressRule struct {
180 Hostname string `json:"hostname,omitempty"`
181 Path string `json:"path,omitempty"`
182 Service string `json:"service,omitempty"`
183 OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
184}
185
186// OriginRequestConfig is a set of optional fields that users may set to
187// customize how cloudflared sends requests to origin services. It is used to set
188// up general config that apply to all rules, and also, specific per-rule
189// config.
190// Note:
191// - To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
192// - To specify a time.Duration in json, use int64 of the nanoseconds
193type OriginRequestConfig struct {
194 // HTTP proxy timeout for establishing a new connection
195 ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
196 // HTTP proxy timeout for completing a TLS handshake
197 TLSTimeout *CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout,omitempty"`
198 // HTTP proxy TCP keepalive duration
199 TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
200 // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
201 NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs,omitempty"`
202 // HTTP proxy maximum keepalive connection pool size
203 KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections,omitempty"`
204 // HTTP proxy timeout for closing an idle connection
205 KeepAliveTimeout *CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout,omitempty"`
206 // Sets the HTTP Host header for the local webserver.
207 HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
208 // Hostname on the origin server certificate.
209 OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
210 // Path to the CA for the certificate of your origin.
211 // This option should be used only if your certificate is not signed by Cloudflare.
212 CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
213 // Disables TLS verification of the certificate presented by your origin.
214 // Will allow any certificate from the origin to be accepted.
215 // Note: The connection from your machine to Cloudflare's Edge is still encrypted.
216 NoTLSVerify *bool `yaml:"noTLSVerify" json:"noTLSVerify,omitempty"`
217 // Disables chunked transfer encoding.
218 // Useful if you are running a WSGI server.
219 DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding,omitempty"`
220 // Runs as jump host
221 BastionMode *bool `yaml:"bastionMode" json:"bastionMode,omitempty"`
222 // Listen address for the proxy.
223 ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress,omitempty"`
224 // Listen port for the proxy.
225 ProxyPort *uint `yaml:"proxyPort" json:"proxyPort,omitempty"`
226 // Valid options are 'socks' or empty.
227 ProxyType *string `yaml:"proxyType" json:"proxyType,omitempty"`
228 // IP rules for the proxy service
229 IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules,omitempty"`
230}
231
232type IngressIPRule struct {
233 Prefix *string `yaml:"prefix" json:"prefix"`
234 Ports []int `yaml:"ports" json:"ports"`
235 Allow bool `yaml:"allow" json:"allow"`
236}
237
238type Configuration struct {
239 TunnelID string `yaml:"tunnel"`
240 Ingress []UnvalidatedIngressRule
241 WarpRouting WarpRoutingConfig `yaml:"warp-routing"`
242 OriginRequest OriginRequestConfig `yaml:"originRequest"`
243 sourceFile string
244}
245
246type WarpRoutingConfig struct {
247 Enabled bool `yaml:"enabled" json:"enabled"`
248 ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
249 TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
250}
251
252type configFileSettings struct {
253 Configuration `yaml:",inline"`
254 // older settings will be aggregated into the generic map, should be read via cli.Context
255 Settings map[string]interface{} `yaml:",inline"`
256}
257
258func (c *Configuration) Source() string {
259 return c.sourceFile
260}
261
262func (c *configFileSettings) Int(name string) (int, error) {
263 if raw, ok := c.Settings[name]; ok {
264 if v, ok := raw.(int); ok {
265 return v, nil
266 }
267 return 0, fmt.Errorf("expected int found %T for %s", raw, name)
268 }
269 return 0, nil
270}
271
272func (c *configFileSettings) Duration(name string) (time.Duration, error) {
273 if raw, ok := c.Settings[name]; ok {
274 switch v := raw.(type) {
275 case time.Duration:
276 return v, nil
277 case string:
278 return time.ParseDuration(v)
279 }
280 return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
281 }
282 return 0, nil
283}
284
285func (c *configFileSettings) Float64(name string) (float64, error) {
286 if raw, ok := c.Settings[name]; ok {
287 if v, ok := raw.(float64); ok {
288 return v, nil
289 }
290 return 0, fmt.Errorf("expected float found %T for %s", raw, name)
291 }
292 return 0, nil
293}
294
295func (c *configFileSettings) String(name string) (string, error) {
296 if raw, ok := c.Settings[name]; ok {
297 if v, ok := raw.(string); ok {
298 return v, nil
299 }
300 return "", fmt.Errorf("expected string found %T for %s", raw, name)
301 }
302 return "", nil
303}
304
305func (c *configFileSettings) StringSlice(name string) ([]string, error) {
306 if raw, ok := c.Settings[name]; ok {
307 if slice, ok := raw.([]interface{}); ok {
308 strSlice := make([]string, len(slice))
309 for i, v := range slice {
310 str, ok := v.(string)
311 if !ok {
312 return nil, fmt.Errorf("expected string, found %T for %v", i, v)
313 }
314 strSlice[i] = str
315 }
316 return strSlice, nil
317 }
318 return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
319 }
320 return nil, nil
321}
322
323func (c *configFileSettings) IntSlice(name string) ([]int, error) {
324 if raw, ok := c.Settings[name]; ok {
325 if slice, ok := raw.([]interface{}); ok {
326 intSlice := make([]int, len(slice))
327 for i, v := range slice {
328 str, ok := v.(int)
329 if !ok {
330 return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
331 }
332 intSlice[i] = str
333 }
334 return intSlice, nil
335 }
336 if v, ok := raw.([]int); ok {
337 return v, nil
338 }
339 return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
340 }
341 return nil, nil
342}
343
344func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
345 return nil, errors.New("option type Generic not supported")
346}
347
348func (c *configFileSettings) Bool(name string) (bool, error) {
349 if raw, ok := c.Settings[name]; ok {
350 if v, ok := raw.(bool); ok {
351 return v, nil
352 }
353 return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
354 }
355 return false, nil
356}
357
358var configuration configFileSettings
359
360func GetConfiguration() *Configuration {
361 return &configuration.Configuration
362}
363
364// ReadConfigFile returns InputSourceContext initialized from the configuration file.
365// On repeat calls returns with the same file, returns without reading the file again; however,
366// if value of "config" flag changes, will read the new config file
367func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
368 configFile := c.String("config")
369 if configuration.Source() == configFile || configFile == "" {
370 if configuration.Source() == "" {
371 return nil, "", ErrNoConfigFile
372 }
373 return &configuration, "", nil
374 }
375
376 log.Debug().Msgf("Loading configuration from %s", configFile)
377 file, err := os.Open(configFile)
378 if err != nil {
379 if os.IsNotExist(err) {
380 err = ErrNoConfigFile
381 }
382 return nil, "", err
383 }
384 defer file.Close()
385 if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
386 if err == io.EOF {
387 log.Error().Msgf("Configuration file %s was empty", configFile)
388 return &configuration, "", nil
389 }
390 return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
391 }
392 configuration.sourceFile = configFile
393
394 // Parse it again, with strict mode, to find warnings.
395 if file, err := os.Open(configFile); err == nil {
396 decoder := yaml.NewDecoder(file)
397 decoder.KnownFields(true)
398 var unusedConfig configFileSettings
399 if err := decoder.Decode(&unusedConfig); err != nil {
400 warnings = err.Error()
401 }
402 }
403
404 return &configuration, warnings, nil
405}
406
407// A CustomDuration is a Duration that has custom serialization for JSON.
408// JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers
409// are in nanoseconds, which in 32bit integers limits to just 2 seconds.
410// This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains
411// the YAML serde assumptions.
412type CustomDuration struct {
413 time.Duration
414}
415
416func (s CustomDuration) MarshalJSON() ([]byte, error) {
417 return json.Marshal(s.Duration.Seconds())
418}
419
420func (s *CustomDuration) UnmarshalJSON(data []byte) error {
421 seconds, err := strconv.ParseInt(string(data), 10, 64)
422 if err != nil {
423 return err
424 }
425
426 s.Duration = time.Duration(seconds * int64(time.Second))
427 return nil
428}
429
430func (s *CustomDuration) MarshalYAML() (interface{}, error) {
431 return s.Duration.String(), nil
432}
433
434func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
435 return unmarshal(&s.Duration)
436}
437