1// Licensed to Elasticsearch B.V. under one or more contributor
2// license agreements. See the NOTICE file distributed with
3// this work for additional information regarding copyright
4// ownership. Elasticsearch B.V. licenses this file to you under
5// the Apache License, Version 2.0 (the "License"); you may
6// not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18package apm
19
20import (
21	"fmt"
22	"net/http"
23
24	"go.elastic.co/apm/internal/apmhttputil"
25	"go.elastic.co/apm/model"
26)
27
28// Context provides methods for setting transaction and error context.
29//
30// NOTE this is entirely unrelated to the standard library's context.Context.
31type Context struct {
32	model            model.Context
33	request          model.Request
34	requestBody      model.RequestBody
35	requestSocket    model.RequestSocket
36	response         model.Response
37	user             model.User
38	service          model.Service
39	serviceFramework model.Framework
40	captureHeaders   bool
41	captureBodyMask  CaptureBodyMode
42}
43
44func (c *Context) build() *model.Context {
45	switch {
46	case c.model.Request != nil:
47	case c.model.Response != nil:
48	case c.model.User != nil:
49	case c.model.Service != nil:
50	case len(c.model.Tags) != 0:
51	case len(c.model.Custom) != 0:
52	default:
53		return nil
54	}
55	return &c.model
56}
57
58func (c *Context) reset() {
59	*c = Context{
60		model: model.Context{
61			Custom: c.model.Custom[:0],
62			Tags:   c.model.Tags[:0],
63		},
64		captureBodyMask: c.captureBodyMask,
65		request: model.Request{
66			Headers: c.request.Headers[:0],
67		},
68		response: model.Response{
69			Headers: c.response.Headers[:0],
70		},
71	}
72}
73
74// SetTag sets a tag in the context. Invalid characters
75// ('.', '*', and '"') in the key will be replaced with
76// an underscore.
77func (c *Context) SetTag(key, value string) {
78	// Note that we do not attempt to de-duplicate the keys.
79	// This is OK, since json.Unmarshal will always take the
80	// final instance.
81	c.model.Tags = append(c.model.Tags, model.StringMapItem{
82		Key:   cleanTagKey(key),
83		Value: truncateString(value),
84	})
85}
86
87// SetCustom sets custom context.
88//
89// Invalid characters ('.', '*', and '"') in the key will be
90// replaced with an underscore. The value may be any JSON-encodable
91// value.
92func (c *Context) SetCustom(key string, value interface{}) {
93	// Note that we do not attempt to de-duplicate the keys.
94	// This is OK, since json.Unmarshal will always take the
95	// final instance.
96	c.model.Custom = append(c.model.Custom, model.IfaceMapItem{
97		Key:   cleanTagKey(key),
98		Value: value,
99	})
100}
101
102// SetFramework sets the framework name and version in the context.
103//
104// This is used for identifying the framework in which the context
105// was created, such as Gin or Echo.
106//
107// If the name is empty, this is a no-op. If version is empty, then
108// it will be set to "unspecified".
109func (c *Context) SetFramework(name, version string) {
110	if name == "" {
111		return
112	}
113	if version == "" {
114		// Framework version is required.
115		version = "unspecified"
116	}
117	c.serviceFramework = model.Framework{
118		Name:    truncateString(name),
119		Version: truncateString(version),
120	}
121	c.service.Framework = &c.serviceFramework
122	c.model.Service = &c.service
123}
124
125// SetHTTPRequest sets details of the HTTP request in the context.
126//
127// This function relates to server-side requests. Various proxy
128// forwarding headers are taken into account to reconstruct the URL,
129// and determining the client address.
130//
131// If the request URL contains user info, it will be removed and
132// excluded from the URL's "full" field.
133//
134// If the request contains HTTP Basic Authentication, the username
135// from that will be recorded in the context. Otherwise, if the
136// request contains user info in the URL (i.e. a client-side URL),
137// that will be used.
138func (c *Context) SetHTTPRequest(req *http.Request) {
139	// Special cases to avoid calling into fmt.Sprintf in most cases.
140	var httpVersion string
141	switch {
142	case req.ProtoMajor == 1 && req.ProtoMinor == 1:
143		httpVersion = "1.1"
144	case req.ProtoMajor == 2 && req.ProtoMinor == 0:
145		httpVersion = "2.0"
146	default:
147		httpVersion = fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor)
148	}
149
150	var forwarded *apmhttputil.ForwardedHeader
151	if fwd := req.Header.Get("Forwarded"); fwd != "" {
152		parsed := apmhttputil.ParseForwarded(fwd)
153		forwarded = &parsed
154	}
155	c.request = model.Request{
156		Body:        c.request.Body,
157		URL:         apmhttputil.RequestURL(req, forwarded),
158		Method:      truncateString(req.Method),
159		HTTPVersion: httpVersion,
160		Cookies:     req.Cookies(),
161	}
162	c.model.Request = &c.request
163
164	if c.captureHeaders {
165		for k, values := range req.Header {
166			if k == "Cookie" {
167				// We capture cookies in the request structure.
168				continue
169			}
170			c.request.Headers = append(c.request.Headers, model.Header{
171				Key: k, Values: values,
172			})
173		}
174	}
175
176	c.requestSocket = model.RequestSocket{
177		Encrypted:     req.TLS != nil,
178		RemoteAddress: apmhttputil.RemoteAddr(req, forwarded),
179	}
180	if c.requestSocket != (model.RequestSocket{}) {
181		c.request.Socket = &c.requestSocket
182	}
183
184	username, _, ok := req.BasicAuth()
185	if !ok && req.URL.User != nil {
186		username = req.URL.User.Username()
187	}
188	c.user.Username = truncateString(username)
189	if c.user.Username != "" {
190		c.model.User = &c.user
191	}
192}
193
194// SetHTTPRequestBody sets the request body in context given a (possibly nil)
195// BodyCapturer returned by Tracer.CaptureHTTPRequestBody.
196func (c *Context) SetHTTPRequestBody(bc *BodyCapturer) {
197	if bc == nil || bc.captureBody&c.captureBodyMask == 0 {
198		return
199	}
200	if bc.setContext(&c.requestBody) {
201		c.request.Body = &c.requestBody
202	}
203}
204
205// SetHTTPResponseHeaders sets the HTTP response headers in the context.
206func (c *Context) SetHTTPResponseHeaders(h http.Header) {
207	if !c.captureHeaders {
208		return
209	}
210	for k, values := range h {
211		c.response.Headers = append(c.response.Headers, model.Header{
212			Key: k, Values: values,
213		})
214	}
215	if len(c.response.Headers) != 0 {
216		c.model.Response = &c.response
217	}
218}
219
220// SetHTTPStatusCode records the HTTP response status code.
221func (c *Context) SetHTTPStatusCode(statusCode int) {
222	c.response.StatusCode = statusCode
223	c.model.Response = &c.response
224}
225
226// SetUserID sets the ID of the authenticated user.
227func (c *Context) SetUserID(id string) {
228	c.user.ID = truncateString(id)
229	if c.user.ID != "" {
230		c.model.User = &c.user
231	}
232}
233
234// SetUserEmail sets the email for the authenticated user.
235func (c *Context) SetUserEmail(email string) {
236	c.user.Email = truncateString(email)
237	if c.user.Email != "" {
238		c.model.User = &c.user
239	}
240}
241
242// SetUsername sets the username of the authenticated user.
243func (c *Context) SetUsername(username string) {
244	c.user.Username = truncateString(username)
245	if c.user.Username != "" {
246		c.model.User = &c.user
247	}
248}
249