Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add HTTP basic auth support to the http_listener input plugin #3496

Merged
merged 1 commit into from
Mar 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions etc/telegraf.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2574,6 +2574,11 @@
# ## Add service certificate and key
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
#
# ## Optional username and password to accept for HTTP basic authentication.
# ## You probably want to make sure you have TLS configured above for this.
# basic_username = "foobar"
# basic_password = "barfoo"


# # Read metrics from Kafka topic(s)
Expand Down
6 changes: 6 additions & 0 deletions plugins/inputs/http_listener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Enable TLS by specifying the file names of a service TLS certificate and key.

Enable mutually authenticated TLS and authorize client connections by signing certificate authority by including a list of allowed CA certificate file names in ````tls_allowed_cacerts````.

Enable basic HTTP authentication of clients by specifying a username and password to check for. These credentials will be received from the client _as plain text_ if TLS is not configured.

See: [Telegraf Input Data Formats](https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md#influx).

**Example:**
Expand Down Expand Up @@ -39,4 +41,8 @@ This is a sample configuration for the plugin.

## MTLS
tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]

## Basic authentication
basic_username = "foobar"
basic_password = "barfoo"
```
46 changes: 39 additions & 7 deletions plugins/inputs/http_listener/http_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http_listener
import (
"bytes"
"compress/gzip"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"io"
Expand Down Expand Up @@ -44,6 +45,9 @@ type HTTPListener struct {
TlsCert string
TlsKey string

BasicUsername string
BasicPassword string

mu sync.Mutex
wg sync.WaitGroup

Expand All @@ -64,6 +68,7 @@ type HTTPListener struct {
PingsRecv selfstat.Stat
NotFoundsServed selfstat.Stat
BuffersCreated selfstat.Stat
AuthFailures selfstat.Stat
}

const sampleConfig = `
Expand All @@ -90,6 +95,11 @@ const sampleConfig = `
## Add service certificate and key
tls_cert = "/etc/telegraf/cert.pem"
tls_key = "/etc/telegraf/key.pem"

## Optional username and password to accept for HTTP basic authentication.
## You probably want to make sure you have TLS configured above for this.
# basic_username = "foobar"
# basic_password = "barfoo"
`

func (h *HTTPListener) SampleConfig() string {
Expand Down Expand Up @@ -124,6 +134,7 @@ func (h *HTTPListener) Start(acc telegraf.Accumulator) error {
h.PingsRecv = selfstat.Register("http_listener", "pings_received", tags)
h.NotFoundsServed = selfstat.Register("http_listener", "not_founds_served", tags)
h.BuffersCreated = selfstat.Register("http_listener", "buffers_created", tags)
h.AuthFailures = selfstat.Register("http_listener", "auth_failures", tags)

if h.MaxBodySize == 0 {
h.MaxBodySize = DEFAULT_MAX_BODY_SIZE
Expand Down Expand Up @@ -194,25 +205,29 @@ func (h *HTTPListener) ServeHTTP(res http.ResponseWriter, req *http.Request) {
case "/write":
h.WritesRecv.Incr(1)
defer h.WritesServed.Incr(1)
h.serveWrite(res, req)
h.AuthenticateIfSet(h.serveWrite, res, req)
case "/query":
h.QueriesRecv.Incr(1)
defer h.QueriesServed.Incr(1)
// Deliver a dummy response to the query endpoint, as some InfluxDB
// clients test endpoint availability with a query
res.Header().Set("Content-Type", "application/json")
res.Header().Set("X-Influxdb-Version", "1.0")
res.WriteHeader(http.StatusOK)
res.Write([]byte("{\"results\":[]}"))
h.AuthenticateIfSet(func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json")
res.Header().Set("X-Influxdb-Version", "1.0")
res.WriteHeader(http.StatusOK)
res.Write([]byte("{\"results\":[]}"))
}, res, req)
case "/ping":
h.PingsRecv.Incr(1)
defer h.PingsServed.Incr(1)
// respond to ping requests
res.WriteHeader(http.StatusNoContent)
h.AuthenticateIfSet(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusNoContent)
}, res, req)
default:
defer h.NotFoundsServed.Incr(1)
// Don't know how to respond to calls to other endpoints
http.NotFound(res, req)
h.AuthenticateIfSet(http.NotFound, res, req)
}
}

Expand Down Expand Up @@ -376,6 +391,23 @@ func (h *HTTPListener) getTLSConfig() *tls.Config {
return tlsConf
}

func (h *HTTPListener) AuthenticateIfSet(handler http.HandlerFunc, res http.ResponseWriter, req *http.Request) {
if h.BasicUsername != "" && h.BasicPassword != "" {
reqUsername, reqPassword, ok := req.BasicAuth()
if !ok ||
subtle.ConstantTimeCompare([]byte(reqUsername), []byte(h.BasicUsername)) != 1 ||
subtle.ConstantTimeCompare([]byte(reqPassword), []byte(h.BasicPassword)) != 1 {

h.AuthFailures.Incr(1)
http.Error(res, "Unauthorized.", http.StatusUnauthorized)
return
}
handler(res, req)
} else {
handler(res, req)
}
}

func init() {
inputs.Add("http_listener", func() telegraf.Input {
return &HTTPListener{
Expand Down
28 changes: 28 additions & 0 deletions plugins/inputs/http_listener/http_listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ NsFlcGACj+/TvacFYlA6N2nyFeokzoqLX28Ddxdh2erXqJ4hYIhT1ik9tkLggs2z
1T1084BquCuO6lIcOwJBALX4xChoMUF9k0IxSQzlz//seQYDkQNsE7y9IgAOXkzp
RaR4pzgPbnKj7atG+2dBnffWfE+1Mcy0INDAO6WxPg0=
-----END RSA PRIVATE KEY-----`

basicUsername = "test-username-please-ignore"
basicPassword = "super-secure-password!"
)

var (
Expand All @@ -119,6 +122,13 @@ func newTestHTTPListener() *HTTPListener {
return listener
}

func newTestHTTPAuthListener() *HTTPListener {
listener := newTestHTTPListener()
listener.BasicUsername = basicUsername
listener.BasicPassword = basicPassword
return listener
}

func newTestHTTPSListener() *HTTPListener {
initServiceCertFiles.Do(func() {
acaf, err := ioutil.TempFile("", "allowedCAFile.crt")
Expand Down Expand Up @@ -238,6 +248,24 @@ func TestWriteHTTPSWithClientAuth(t *testing.T) {
require.EqualValues(t, 204, resp.StatusCode)
}

func TestWriteHTTPBasicAuth(t *testing.T) {
listener := newTestHTTPAuthListener()

acc := &testutil.Accumulator{}
require.NoError(t, listener.Start(acc))
defer listener.Stop()

client := &http.Client{}

req, err := http.NewRequest("POST", createURL(listener, "http", "/write", "db=mydb"), bytes.NewBuffer([]byte(testMsg)))
require.NoError(t, err)
req.SetBasicAuth(basicUsername, basicPassword)
resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
require.EqualValues(t, http.StatusNoContent, resp.StatusCode)
}

func TestWriteHTTP(t *testing.T) {
listener := newTestHTTPListener()

Expand Down