Skip to content

Commit

Permalink
Record all HTTP request/response headers
Browse files Browse the repository at this point in the history
Record all HTTP request/response headers, with the
exception of "Cookie", which we explode and store
separately.

All headers pass through the field sanitiser. If a
header is included multiple times, we store the
values as an array of strings. Otherwise, we store
just a single string value.
  • Loading branch information
axw committed Oct 26, 2018
1 parent 57729c3 commit 5dabc6a
Show file tree
Hide file tree
Showing 16 changed files with 221 additions and 151 deletions.
33 changes: 20 additions & 13 deletions context.go
Expand Up @@ -3,7 +3,6 @@ package apm
import (
"fmt"
"net/http"
"strings"

"go.elastic.co/apm/internal/apmhttputil"
"go.elastic.co/apm/model"
Expand All @@ -14,10 +13,8 @@ type Context struct {
model model.Context
request model.Request
requestBody model.RequestBody
requestHeaders model.RequestHeaders
requestSocket model.RequestSocket
response model.Response
responseHeaders model.ResponseHeaders
user model.User
service model.Service
serviceFramework model.Framework
Expand Down Expand Up @@ -46,6 +43,12 @@ func (c *Context) reset() {
*c = Context{
model: modelContext,
captureBodyMask: c.captureBodyMask,
request: model.Request{
Headers: c.request.Headers[:0],
},
response: model.Response{
Headers: c.response.Headers[:0],
},
}
}

Expand Down Expand Up @@ -140,13 +143,14 @@ func (c *Context) SetHTTPRequest(req *http.Request) {
}
c.model.Request = &c.request

c.requestHeaders = model.RequestHeaders{
ContentType: req.Header.Get("Content-Type"),
Cookie: truncateString(strings.Join(req.Header["Cookie"], ";")),
UserAgent: req.UserAgent(),
}
if c.requestHeaders != (model.RequestHeaders{}) {
c.request.Headers = &c.requestHeaders
for k, values := range req.Header {
if k == "Cookie" {
// We capture cookies in the request structure.
continue
}
c.request.Headers = append(c.request.Headers, model.Header{
Key: k, Values: values,
})
}

c.requestSocket = model.RequestSocket{
Expand Down Expand Up @@ -180,9 +184,12 @@ func (c *Context) SetHTTPRequestBody(bc *BodyCapturer) {

// SetHTTPResponseHeaders sets the HTTP response headers in the context.
func (c *Context) SetHTTPResponseHeaders(h http.Header) {
c.responseHeaders.ContentType = h.Get("Content-Type")
if c.responseHeaders.ContentType != "" {
c.response.Headers = &c.responseHeaders
for k, values := range h {
c.response.Headers = append(c.response.Headers, model.Header{
Key: k, Values: values,
})
}
if len(c.response.Headers) != 0 {
c.model.Response = &c.response
}
}
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.asciidoc
Expand Up @@ -183,8 +183,8 @@ Prefixing a pattern with `(?-i)` makes the matching case sensitive.

[options="header"]
|============
| Environment | Default | Example
| `ELASTIC_APM_SANITIZE_FIELD_NAMES` | `password, passwd, pwd, secret, *key, *token*, *session*, *credit*, *card*` | `sekrits`
| Environment | Default | Example
| `ELASTIC_APM_SANITIZE_FIELD_NAMES` | `password, passwd, pwd, secret, *key, *token*, *session*, *credit*, *card*, authorization, set-cookie` | `sekrits`
|============

A list of patterns to match the names of HTTP cookies and POST form fields to refact.
Expand Down
2 changes: 2 additions & 0 deletions env.go
Expand Up @@ -54,6 +54,8 @@ var (
"*session*",
"*credit*",
"*card*",
"authorization",
"set-cookie",
}, ","))
)

Expand Down
68 changes: 68 additions & 0 deletions model/marshal.go
Expand Up @@ -295,6 +295,74 @@ func (c *Cookies) UnmarshalJSON(data []byte) error {
return nil
}

func (hs Headers) isZero() bool {
return len(hs) == 0
}

// MarshalFastJSON writes the JSON representation of h to w.
func (hs Headers) MarshalFastJSON(w *fastjson.Writer) error {
w.RawByte('{')
for i, h := range hs {
if i != 0 {
w.RawByte(',')
}
w.String(h.Key)
w.RawByte(':')
if len(h.Values) == 1 {
// Just one item, add the item directly.
w.String(h.Values[0])
} else {
// Zero or multiple items, include them all.
w.RawByte('[')
for i, v := range h.Values {
if i != 0 {
w.RawByte(',')
}
w.String(v)
}
w.RawByte(']')
}
}
w.RawByte('}')
return nil
}

// MarshalFastJSON writes the JSON representation of h to w.
func (*Header) MarshalFastJSON(w *fastjson.Writer) error {
panic("unreachable")
}

// UnmarshalJSON unmarshals the JSON data into c.
func (hs *Headers) UnmarshalJSON(data []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
for k, v := range m {
switch v := v.(type) {
case string:
*hs = append(*hs, Header{Key: k, Values: []string{v}})
case []interface{}:
var values []string
for _, v := range v {
switch v := v.(type) {
case string:
values = append(values, v)
default:
return errors.Errorf("expected string, got %T", v)
}
}
*hs = append(*hs, Header{Key: k, Values: values})
default:
return errors.Errorf("expected string or []string, got %T", v)
}
}
sort.Slice(*hs, func(i, j int) bool {
return (*hs)[i].Key < (*hs)[j].Key
})
return nil
}

// MarshalFastJSON writes the JSON representation of c to w.
func (c *ExceptionCode) MarshalFastJSON(w *fastjson.Writer) error {
if c.String != "" {
Expand Down
51 changes: 2 additions & 49 deletions model/marshal_fastjson.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 16 additions & 14 deletions model/marshal_test.go
Expand Up @@ -50,8 +50,8 @@ func TestMarshalTransaction(t *testing.T) {
},
"method": "GET",
"headers": map[string]interface{}{
"user-agent": "Mosaic/0.2 (Windows 3.1)",
"cookie": "monster=yumyum; random=junk",
"User-Agent": "Mosaic/0.2 (Windows 3.1)",
"Cookie": "monster=yumyum; random=junk",
},
"body": "ahoj",
"http_version": "1.1",
Expand All @@ -67,7 +67,7 @@ func TestMarshalTransaction(t *testing.T) {
"response": map[string]interface{}{
"status_code": float64(418),
"headers": map[string]interface{}{
"content-type": "text/html",
"Content-Type": "text/html",
},
},
"user": map[string]interface{}{
Expand Down Expand Up @@ -393,16 +393,17 @@ func TestMarshalResponse(t *testing.T) {
headersSent := true
response := model.Response{
Finished: &finished,
Headers: &model.ResponseHeaders{
ContentType: "text/plain",
},
Headers: model.Headers{{
Key: "Content-Type",
Values: []string{"text/plain"},
}},
HeadersSent: &headersSent,
StatusCode: 200,
}
var w fastjson.Writer
response.MarshalFastJSON(&w)
assert.Equal(t,
`{"finished":true,"headers":{"content-type":"text/plain"},"headers_sent":true,"status_code":200}`,
`{"finished":true,"headers":{"Content-Type":"text/plain"},"headers_sent":true,"status_code":200}`,
string(w.Bytes()),
)
}
Expand Down Expand Up @@ -507,10 +508,11 @@ func fakeTransaction() model.Transaction {
Hash: "qux",
},
Method: "GET",
Headers: &model.RequestHeaders{
UserAgent: "Mosaic/0.2 (Windows 3.1)",
Cookie: "monster=yumyum; random=junk",
},
Headers: model.Headers{{
Key: "Cookie", Values: []string{"monster=yumyum; random=junk"},
}, {
Key: "User-Agent", Values: []string{"Mosaic/0.2 (Windows 3.1)"},
}},
Body: &model.RequestBody{
Raw: "ahoj",
},
Expand All @@ -526,9 +528,9 @@ func fakeTransaction() model.Transaction {
},
Response: &model.Response{
StatusCode: 418,
Headers: &model.ResponseHeaders{
ContentType: "text/html",
},
Headers: model.Headers{{
Key: "Content-Type", Values: []string{"text/html"},
}},
},
User: &model.User{
Username: "wanda",
Expand Down
26 changes: 8 additions & 18 deletions model/model.go
Expand Up @@ -396,7 +396,7 @@ type Request struct {
Method string `json:"method"`

// Headers holds the request headers.
Headers *RequestHeaders `json:"headers,omitempty"`
Headers Headers `json:"headers,omitempty"`

// Body holds the request body, if body capture is enabled.
Body *RequestBody `json:"body,omitempty"`
Expand Down Expand Up @@ -429,17 +429,13 @@ type RequestBody struct {
Form url.Values
}

// RequestHeaders holds a limited subset of HTTP request headers.
type RequestHeaders struct {
// ContentType holds the content-type header.
ContentType string `json:"content-type,omitempty"`
// Headers holds a collection of HTTP headers.
type Headers []Header

// Cookie holds the cookies sent with the request,
// delimited by semi-colons.
Cookie string `json:"cookie,omitempty"`

// UserAgent holds the user-agent header.
UserAgent string `json:"user-agent,omitempty"`
// Header holds an HTTP header, with one or more values.
type Header struct {
Key string
Values []string
}

// RequestSocket holds transport-level information relating to an HTTP request.
Expand Down Expand Up @@ -485,7 +481,7 @@ type Response struct {
StatusCode int `json:"status_code,omitempty"`

// Headers holds the response headers.
Headers *ResponseHeaders `json:"headers,omitempty"`
Headers Headers `json:"headers,omitempty"`

// HeadersSent indicates whether or not headers were sent
// to the client.
Expand All @@ -495,12 +491,6 @@ type Response struct {
Finished *bool `json:"finished,omitempty"`
}

// ResponseHeaders holds a limited subset of HTTP respponse headers.
type ResponseHeaders struct {
// ContentType holds the content-type header.
ContentType string `json:"content-type,omitempty"`
}

// Time is a timestamp, formatted as a number of microseconds since January 1, 1970 UTC.
type Time time.Time

Expand Down
9 changes: 7 additions & 2 deletions modelwriter.go
Expand Up @@ -91,8 +91,13 @@ func (w *modelWriter) buildModelTransaction(out *model.Transaction, tx *Transact
}

out.Context = tx.Context.build()
if len(w.cfg.sanitizedFieldNames) != 0 && out.Context != nil && out.Context.Request != nil {
sanitizeRequest(out.Context.Request, w.cfg.sanitizedFieldNames)
if len(w.cfg.sanitizedFieldNames) != 0 && out.Context != nil {
if out.Context.Request != nil {
sanitizeRequest(out.Context.Request, w.cfg.sanitizedFieldNames)
}
if out.Context.Response != nil {
sanitizeResponse(out.Context.Response, w.cfg.sanitizedFieldNames)
}
}
}

Expand Down

0 comments on commit 5dabc6a

Please sign in to comment.