1package httpd
2
3import (
4	"bytes"
5	"crypto/tls"
6	"errors"
7	"fmt"
8	"regexp"
9	"strconv"
10	"time"
11
12	"github.com/influxdata/influxdb/monitor/diagnostics"
13	"github.com/influxdata/influxdb/toml"
14)
15
16const (
17	// DefaultBindAddress is the default address to bind to.
18	DefaultBindAddress = ":8086"
19
20	// DefaultRealm is the default realm sent back when issuing a basic auth challenge.
21	DefaultRealm = "InfluxDB"
22
23	// DefaultBindSocket is the default unix socket to bind to.
24	DefaultBindSocket = "/var/run/influxdb.sock"
25
26	// DefaultMaxBodySize is the default maximum size of a client request body, in bytes. Specify 0 for no limit.
27	DefaultMaxBodySize = 25e6
28
29	// DefaultEnqueuedWriteTimeout is the maximum time a write request can wait to be processed.
30	DefaultEnqueuedWriteTimeout = 30 * time.Second
31)
32
33// Config represents a configuration for a HTTP service.
34type Config struct {
35	Enabled                 bool           `toml:"enabled"`
36	BindAddress             string         `toml:"bind-address"`
37	AuthEnabled             bool           `toml:"auth-enabled"`
38	LogEnabled              bool           `toml:"log-enabled"`
39	SuppressWriteLog        bool           `toml:"suppress-write-log"`
40	WriteTracing            bool           `toml:"write-tracing"`
41	FluxEnabled             bool           `toml:"flux-enabled"`
42	FluxLogEnabled          bool           `toml:"flux-log-enabled"`
43	PprofEnabled            bool           `toml:"pprof-enabled"`
44	DebugPprofEnabled       bool           `toml:"debug-pprof-enabled"`
45	HTTPSEnabled            bool           `toml:"https-enabled"`
46	HTTPSCertificate        string         `toml:"https-certificate"`
47	HTTPSPrivateKey         string         `toml:"https-private-key"`
48	MaxRowLimit             int            `toml:"max-row-limit"`
49	MaxConnectionLimit      int            `toml:"max-connection-limit"`
50	SharedSecret            string         `toml:"shared-secret"`
51	Realm                   string         `toml:"realm"`
52	UnixSocketEnabled       bool           `toml:"unix-socket-enabled"`
53	UnixSocketGroup         *toml.Group    `toml:"unix-socket-group"`
54	UnixSocketPermissions   toml.FileMode  `toml:"unix-socket-permissions"`
55	BindSocket              string         `toml:"bind-socket"`
56	MaxBodySize             int            `toml:"max-body-size"`
57	AccessLogPath           string         `toml:"access-log-path"`
58	AccessLogStatusFilters  []StatusFilter `toml:"access-log-status-filters"`
59	MaxConcurrentWriteLimit int            `toml:"max-concurrent-write-limit"`
60	MaxEnqueuedWriteLimit   int            `toml:"max-enqueued-write-limit"`
61	EnqueuedWriteTimeout    time.Duration  `toml:"enqueued-write-timeout"`
62	TLS                     *tls.Config    `toml:"-"`
63}
64
65// NewConfig returns a new Config with default settings.
66func NewConfig() Config {
67	return Config{
68		Enabled:               true,
69		FluxEnabled:           false,
70		FluxLogEnabled:        false,
71		BindAddress:           DefaultBindAddress,
72		LogEnabled:            true,
73		PprofEnabled:          true,
74		DebugPprofEnabled:     false,
75		HTTPSEnabled:          false,
76		HTTPSCertificate:      "/etc/ssl/influxdb.pem",
77		MaxRowLimit:           0,
78		Realm:                 DefaultRealm,
79		UnixSocketEnabled:     false,
80		UnixSocketPermissions: 0777,
81		BindSocket:            DefaultBindSocket,
82		MaxBodySize:           DefaultMaxBodySize,
83		EnqueuedWriteTimeout:  DefaultEnqueuedWriteTimeout,
84	}
85}
86
87// Diagnostics returns a diagnostics representation of a subset of the Config.
88func (c Config) Diagnostics() (*diagnostics.Diagnostics, error) {
89	if !c.Enabled {
90		return diagnostics.RowFromMap(map[string]interface{}{
91			"enabled": false,
92		}), nil
93	}
94
95	return diagnostics.RowFromMap(map[string]interface{}{
96		"enabled":              true,
97		"bind-address":         c.BindAddress,
98		"https-enabled":        c.HTTPSEnabled,
99		"max-row-limit":        c.MaxRowLimit,
100		"max-connection-limit": c.MaxConnectionLimit,
101		"access-log-path":      c.AccessLogPath,
102	}), nil
103}
104
105// StatusFilter will check if an http status code matches a certain pattern.
106type StatusFilter struct {
107	base    int
108	divisor int
109}
110
111// reStatusFilter ensures that the format is digits optionally followed by X values.
112var reStatusFilter = regexp.MustCompile(`^([1-5]\d*)([xX]*)$`)
113
114// ParseStatusFilter will create a new status filter from the string.
115func ParseStatusFilter(s string) (StatusFilter, error) {
116	m := reStatusFilter.FindStringSubmatch(s)
117	if m == nil {
118		return StatusFilter{}, fmt.Errorf("status filter must be a digit that starts with 1-5 optionally followed by X characters")
119	} else if len(s) != 3 {
120		return StatusFilter{}, fmt.Errorf("status filter must be exactly 3 characters long")
121	}
122
123	// Compute the divisor and the expected value. If we have one X, we divide by 10 so we are only comparing
124	// the first two numbers. If we have two Xs, we divide by 100 so we only compare the first number. We
125	// then check if the result is equal to the remaining number.
126	base, err := strconv.Atoi(m[1])
127	if err != nil {
128		return StatusFilter{}, err
129	}
130
131	divisor := 1
132	switch len(m[2]) {
133	case 1:
134		divisor = 10
135	case 2:
136		divisor = 100
137	}
138	return StatusFilter{
139		base:    base,
140		divisor: divisor,
141	}, nil
142}
143
144// Match will check if the status code matches this filter.
145func (sf StatusFilter) Match(statusCode int) bool {
146	if sf.divisor == 0 {
147		return false
148	}
149	return statusCode/sf.divisor == sf.base
150}
151
152// UnmarshalText parses a TOML value into a duration value.
153func (sf *StatusFilter) UnmarshalText(text []byte) error {
154	f, err := ParseStatusFilter(string(text))
155	if err != nil {
156		return err
157	}
158	*sf = f
159	return nil
160}
161
162// MarshalText converts a duration to a string for decoding toml
163func (sf StatusFilter) MarshalText() (text []byte, err error) {
164	var buf bytes.Buffer
165	if sf.base != 0 {
166		buf.WriteString(strconv.Itoa(sf.base))
167	}
168
169	switch sf.divisor {
170	case 1:
171	case 10:
172		buf.WriteString("X")
173	case 100:
174		buf.WriteString("XX")
175	default:
176		return nil, errors.New("invalid status filter")
177	}
178	return buf.Bytes(), nil
179}
180
181type StatusFilters []StatusFilter
182
183func (filters StatusFilters) Match(statusCode int) bool {
184	if len(filters) == 0 {
185		return true
186	}
187
188	for _, sf := range filters {
189		if sf.Match(statusCode) {
190			return true
191		}
192	}
193	return false
194}
195