github.com/waldiirawan/apm-agent-go/v2@v2.2.2/transport/http.go (about)

     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  
    18  package transport // import "github.com/waldiirawan/apm-agent-go/v2/transport"
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"encoding/json"
    26  	"encoding/pem"
    27  	"fmt"
    28  	"io"
    29  	"io/ioutil"
    30  	"math/rand"
    31  	"mime/multipart"
    32  	"net/http"
    33  	"net/textproto"
    34  	"net/url"
    35  	"os"
    36  	"path"
    37  	"strconv"
    38  	"strings"
    39  	"sync/atomic"
    40  	"time"
    41  
    42  	"github.com/pkg/errors"
    43  
    44  	"github.com/waldiirawan/apm-agent-go/v2/apmconfig"
    45  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmversion"
    46  	"github.com/waldiirawan/apm-agent-go/v2/internal/configutil"
    47  )
    48  
    49  const (
    50  	intakePath  = "/intake/v2/events"
    51  	profilePath = "/intake/v2/profile"
    52  	configPath  = "/config/v1/agents"
    53  
    54  	envAPIKey           = "ELASTIC_APM_API_KEY"
    55  	envSecretToken      = "ELASTIC_APM_SECRET_TOKEN"
    56  	envServerURLs       = "ELASTIC_APM_SERVER_URLS"
    57  	envServerURL        = "ELASTIC_APM_SERVER_URL"
    58  	envServerTimeout    = "ELASTIC_APM_SERVER_TIMEOUT"
    59  	envServerCert       = "ELASTIC_APM_SERVER_CERT"
    60  	envVerifyServerCert = "ELASTIC_APM_VERIFY_SERVER_CERT"
    61  	envServerCACert     = "ELASTIC_APM_SERVER_CA_CERT_FILE"
    62  )
    63  
    64  var (
    65  	// Take a copy of the http.DefaultTransport pointer,
    66  	// in case another package replaces the value later.
    67  	defaultHTTPTransport = http.DefaultTransport.(*http.Transport)
    68  
    69  	defaultServerURL, _  = url.Parse("http://localhost:8200")
    70  	defaultServerTimeout = 30 * time.Second
    71  )
    72  
    73  // HTTPTransportOptions for the HTTPTransport.
    74  type HTTPTransportOptions struct {
    75  	// APIKey holds the base64-encoded API Key credential string, used for
    76  	// authenticating the agent. APIKey takes precedence over SecretToken.
    77  	//
    78  	// If unspecified, APIKey will be initialized using the
    79  	// ELASTIC_APM_API_KEY environment variable.
    80  	APIKey string
    81  
    82  	// SecretToken holds the secret token configured in the APM Server, used
    83  	// for authenticating the agent.
    84  	//
    85  	// If unspecified, SecretToken will be initialized using the
    86  	// ELASTIC_APM_SECRET_TOKEN envirohnment variable.
    87  	SecretToken string
    88  
    89  	// ServerURLs holds the URLs for your Elastic APM Server. The Server
    90  	// supports both HTTP and HTTPS. If you use HTTPS, then you may need to
    91  	// configure your client machines so that the server certificate can be
    92  	// verified. You can disable certificate verification with SkipServerVerify.
    93  	//
    94  	// If no URLs are specified, then ServerURLs will be initialized using the
    95  	// ELASTIC_APM_SERVER_URL environment variable, defaulting to
    96  	// "http://localhost:8200" if the environment variable is not set.
    97  	ServerURLs []*url.URL
    98  
    99  	// ServerTimeout holds the timeout for requests made to your Elastic APM
   100  	// server.
   101  	//
   102  	// When set to zero, it will default to 30 seconds. Negative values
   103  	// are not allowed.
   104  	//
   105  	// If ServerTimeout is zero, then it will be initialized using the
   106  	// ELASTIC_APM_SERVER_TIMEOUT environment variable, defaulting to
   107  	// 30 seconds if the environment variable is not set. Negative values are
   108  	// not allowed, and will cause NewHTTPTransport to return an error.
   109  	ServerTimeout time.Duration
   110  
   111  	// TLSClientConfig holds client TLS configuration for use in the HTTP client.
   112  	//
   113  	// If TLS is nil, TLS will be constructed using the following environment
   114  	// variables:
   115  	//
   116  	// - ELASTIC_APM_SERVER_CERT: the path to a PEM-encoded TLS certificate
   117  	//   that must match the APM Server-supplied certificate. This can be used
   118  	//   to pin a self signed certificate.
   119  	//
   120  	// - ELASTIC_APM_SERVER_CA_CERT_FILE: the path to a PEM-encoded TLS
   121  	//   Certificate Authority certificate that will be used for verifying
   122  	//   the server's TLS certificate chain.
   123  	//
   124  	// - ELASTIC_APM_VERIFY_SERVER_CERT: flag to control verification of the
   125  	//   APM Server's TLS certificates. If ELASTIC_APM_SERVER_CERT is defined,
   126  	//   ELASTIC_APM_VERIFY_SERVER_CERT is ignored.
   127  	TLSClientConfig *tls.Config
   128  
   129  	// UserAgent holds the value to use for the User-Agent header.
   130  	//
   131  	// If unspecified, UserAgent will be set to the value returned by
   132  	// DefaultUserAgent().
   133  	UserAgent string
   134  }
   135  
   136  // Validate ensures the HTTPTransportOptions are valid.
   137  func (opts HTTPTransportOptions) Validate() error {
   138  	if opts.ServerTimeout < 0 {
   139  		return errors.New("apm transport options: ServerTimeout must be greater or equal to 0")
   140  	}
   141  	return nil
   142  }
   143  
   144  // HTTPTransport is an implementation of Transport, sending payloads via
   145  // a net/http client.
   146  type HTTPTransport struct {
   147  	// Client exposes the http.Client used by the HTTPTransport for
   148  	// sending requests to the APM Server.
   149  	Client         *http.Client
   150  	intakeHeaders  http.Header
   151  	configHeaders  http.Header
   152  	profileHeaders http.Header
   153  	rootHeaders    http.Header
   154  	shuffleRand    *rand.Rand
   155  
   156  	urlIndex    int32
   157  	intakeURLs  []*url.URL
   158  	configURLs  []*url.URL
   159  	profileURLs []*url.URL
   160  
   161  	majorServerVersion uint32
   162  }
   163  
   164  // NewHTTPTransport returns a new HTTPTransport, initialized with opts,
   165  // which can be used for streaming data to the APM Server.
   166  func NewHTTPTransport(opts HTTPTransportOptions) (*HTTPTransport, error) {
   167  	if opts.APIKey == "" {
   168  		opts.APIKey = os.Getenv(envAPIKey)
   169  	}
   170  	if len(opts.ServerURLs) == 0 {
   171  		serverURLs, err := initServerURLs()
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  		opts.ServerURLs = serverURLs
   176  	}
   177  	if opts.TLSClientConfig == nil {
   178  		tlsClientConfig, err := newEnvTLSClientConfig()
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		opts.TLSClientConfig = tlsClientConfig
   183  	}
   184  	if opts.SecretToken == "" && opts.APIKey == "" {
   185  		opts.SecretToken = os.Getenv(envSecretToken)
   186  	}
   187  	if opts.ServerTimeout == 0 {
   188  		serverTimeout, err := configutil.ParseDurationEnv(envServerTimeout, defaultServerTimeout)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		opts.ServerTimeout = serverTimeout
   193  	}
   194  	return newHTTPTransportOptions(opts)
   195  }
   196  
   197  func newHTTPTransportOptions(opts HTTPTransportOptions) (*HTTPTransport, error) {
   198  	if err := opts.Validate(); err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	// If the ServerTimeout is unspecified, set it to defaultServerTimeout.
   203  	client := &http.Client{
   204  		Timeout: opts.ServerTimeout,
   205  		Transport: &http.Transport{
   206  			Proxy:                 defaultHTTPTransport.Proxy,
   207  			DialContext:           defaultHTTPTransport.DialContext,
   208  			MaxIdleConns:          defaultHTTPTransport.MaxIdleConns,
   209  			IdleConnTimeout:       defaultHTTPTransport.IdleConnTimeout,
   210  			TLSHandshakeTimeout:   defaultHTTPTransport.TLSHandshakeTimeout,
   211  			ExpectContinueTimeout: defaultHTTPTransport.ExpectContinueTimeout,
   212  			TLSClientConfig:       opts.TLSClientConfig,
   213  		},
   214  	}
   215  
   216  	commonHeaders := make(http.Header)
   217  
   218  	if opts.UserAgent == "" {
   219  		opts.UserAgent = DefaultUserAgent()
   220  	}
   221  	commonHeaders.Set("User-Agent", opts.UserAgent)
   222  
   223  	intakeHeaders := copyHeaders(commonHeaders)
   224  	intakeHeaders.Set("Content-Type", "application/x-ndjson")
   225  	intakeHeaders.Set("Content-Encoding", "deflate")
   226  	intakeHeaders.Set("Transfer-Encoding", "chunked")
   227  
   228  	profileHeaders := copyHeaders(commonHeaders)
   229  
   230  	t := &HTTPTransport{
   231  		Client:         client,
   232  		configHeaders:  commonHeaders,
   233  		intakeHeaders:  intakeHeaders,
   234  		profileHeaders: profileHeaders,
   235  		rootHeaders:    copyHeaders(commonHeaders),
   236  	}
   237  	if opts.APIKey != "" {
   238  		t.SetAPIKey(opts.APIKey)
   239  	} else if opts.SecretToken != "" {
   240  		t.SetSecretToken(opts.SecretToken)
   241  	}
   242  
   243  	if len(opts.ServerURLs) == 0 {
   244  		opts.ServerURLs = []*url.URL{defaultServerURL}
   245  	}
   246  	if err := t.SetServerURL(opts.ServerURLs...); err != nil {
   247  		return nil, err
   248  	}
   249  	return t, nil
   250  }
   251  
   252  func newEnvTLSClientConfig() (*tls.Config, error) {
   253  	verifyServerCert, err := configutil.ParseBoolEnv(envVerifyServerCert, true)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	tlsClientConfig := &tls.Config{InsecureSkipVerify: !verifyServerCert}
   258  	if serverCertPath := os.Getenv(envServerCert); serverCertPath != "" {
   259  		serverCert, err := loadCertificate(serverCertPath)
   260  		if err != nil {
   261  			return nil, errors.Wrapf(err, "failed to load certificate from %s", serverCertPath)
   262  		}
   263  		// Disable standard verification, we'll check that the
   264  		// server supplies the exact certificate provided.
   265  		tlsClientConfig.InsecureSkipVerify = true
   266  		tlsClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
   267  			return verifyPeerCertificate(rawCerts, serverCert)
   268  		}
   269  	}
   270  	if serverCACertPath := os.Getenv(envServerCACert); serverCACertPath != "" {
   271  		rootCAs := x509.NewCertPool()
   272  		additionalCerts, err := ioutil.ReadFile(serverCACertPath)
   273  		if err != nil {
   274  			return nil, errors.Wrapf(err, "failed to load root CA file from %s", serverCACertPath)
   275  		}
   276  		if !rootCAs.AppendCertsFromPEM(additionalCerts) {
   277  			return nil, fmt.Errorf("failed to load CA certs from %s", serverCACertPath)
   278  		}
   279  		tlsClientConfig.RootCAs = rootCAs
   280  	}
   281  	return tlsClientConfig, nil
   282  }
   283  
   284  // SetServerURL sets the APM Server URL (or URLs) for sending requests.
   285  // At least one URL must be specified, or the method will return an error.
   286  // The list will be randomly shuffled.
   287  func (t *HTTPTransport) SetServerURL(u ...*url.URL) error {
   288  	if len(u) == 0 {
   289  		return errors.New("SetServerURL expects at least one URL")
   290  	}
   291  	intakeURLs := make([]*url.URL, len(u))
   292  	configURLs := make([]*url.URL, len(u))
   293  	profileURLs := make([]*url.URL, len(u))
   294  	for i, u := range u {
   295  		intakeURLs[i] = urlWithPath(u, intakePath)
   296  		configURLs[i] = urlWithPath(u, configPath)
   297  		profileURLs[i] = urlWithPath(u, profilePath)
   298  	}
   299  	if n := len(intakeURLs); n > 0 {
   300  		if t.shuffleRand == nil {
   301  			t.shuffleRand = rand.New(rand.NewSource(time.Now().UnixNano()))
   302  		}
   303  		for i := n - 1; i > 0; i-- {
   304  			j := t.shuffleRand.Intn(i + 1)
   305  			intakeURLs[i], intakeURLs[j] = intakeURLs[j], intakeURLs[i]
   306  			configURLs[i], configURLs[j] = configURLs[j], configURLs[i]
   307  			profileURLs[i], profileURLs[j] = profileURLs[j], profileURLs[i]
   308  		}
   309  	}
   310  	t.intakeURLs = intakeURLs
   311  	t.configURLs = configURLs
   312  	t.profileURLs = profileURLs
   313  	t.urlIndex = 0
   314  	return nil
   315  }
   316  
   317  // SetUserAgent sets the User-Agent header that will be sent with each request.
   318  func (t *HTTPTransport) SetUserAgent(ua string) {
   319  	t.setCommonHeader("User-Agent", ua)
   320  }
   321  
   322  // SetSecretToken sets the Authorization header with the given secret token.
   323  //
   324  // This overrides the value specified via the ELASTIC_APM_SECRET_TOKEN or
   325  // ELASTIC_APM_API_KEY environment variables, if either are set.
   326  func (t *HTTPTransport) SetSecretToken(secretToken string) {
   327  	if secretToken != "" {
   328  		t.setCommonHeader("Authorization", "Bearer "+secretToken)
   329  	} else {
   330  		t.deleteCommonHeader("Authorization")
   331  	}
   332  }
   333  
   334  // SetAPIKey sets the Authorization header with the given API Key.
   335  //
   336  // This overrides the value specified via the ELASTIC_APM_SECRET_TOKEN or
   337  // ELASTIC_APM_API_KEY environment variables, if either are set.
   338  func (t *HTTPTransport) SetAPIKey(apiKey string) {
   339  	if apiKey != "" {
   340  		t.setCommonHeader("Authorization", "ApiKey "+apiKey)
   341  	} else {
   342  		t.deleteCommonHeader("Authorization")
   343  	}
   344  }
   345  
   346  func (t *HTTPTransport) setCommonHeader(key, value string) {
   347  	t.configHeaders.Set(key, value)
   348  	t.rootHeaders.Set(key, value)
   349  	t.intakeHeaders.Set(key, value)
   350  	t.profileHeaders.Set(key, value)
   351  }
   352  
   353  func (t *HTTPTransport) deleteCommonHeader(key string) {
   354  	t.configHeaders.Del(key)
   355  	t.rootHeaders.Del(key)
   356  	t.intakeHeaders.Del(key)
   357  	t.profileHeaders.Del(key)
   358  }
   359  
   360  // SendStream sends the stream over HTTP. If SendStream returns an error and
   361  // the transport is configured with more than one APM Server URL, then the
   362  // following request will be sent to the next URL in the list.
   363  func (t *HTTPTransport) SendStream(ctx context.Context, r io.Reader) error {
   364  	urlIndex := atomic.LoadInt32(&t.urlIndex)
   365  	intakeURL := t.intakeURLs[urlIndex]
   366  	req := t.newRequest("POST", intakeURL)
   367  	req = requestWithContext(ctx, req)
   368  	req.Header = t.intakeHeaders
   369  	req.Body = ioutil.NopCloser(r)
   370  	if err := t.sendStreamRequest(req); err != nil {
   371  		atomic.StoreInt32(&t.urlIndex, (urlIndex+1)%int32(len(t.intakeURLs)))
   372  		// The remote APM Server url has changed, so we invalidate the local
   373  		// Major Server version cache.
   374  		atomic.StoreUint32(&t.majorServerVersion, 0)
   375  		return err
   376  	}
   377  	return nil
   378  }
   379  
   380  func (t *HTTPTransport) sendStreamRequest(req *http.Request) error {
   381  	resp, err := t.Client.Do(req)
   382  	if err != nil {
   383  		return errors.Wrap(err, "sending event request failed")
   384  	}
   385  	defer resp.Body.Close()
   386  	switch resp.StatusCode {
   387  	case http.StatusOK, http.StatusAccepted:
   388  		return nil
   389  	}
   390  
   391  	result := newHTTPError(resp)
   392  	if resp.StatusCode == http.StatusNotFound && result.Message == "404 page not found" {
   393  		// This may be an old (pre-6.5) APM server
   394  		// that does not support the v2 intake API.
   395  		result.Message = fmt.Sprintf("%s not found (requires APM Server 6.5.0 or newer)", req.URL)
   396  	}
   397  	return result
   398  }
   399  
   400  // SendProfile sends a symbolised pprof profile, encoded as protobuf, and gzip-compressed.
   401  //
   402  // NOTE this is an experimental API, and may be removed in a future minor version, without
   403  // being considered a breaking change.
   404  func (t *HTTPTransport) SendProfile(
   405  	ctx context.Context,
   406  	metadataReader io.Reader,
   407  	profileReaders ...io.Reader,
   408  ) error {
   409  	urlIndex := atomic.LoadInt32(&t.urlIndex)
   410  	profileURL := t.profileURLs[urlIndex]
   411  	req := t.newRequest("POST", profileURL)
   412  	req = requestWithContext(ctx, req)
   413  	req.Header = t.profileHeaders
   414  
   415  	writeBody := func(w *multipart.Writer) error {
   416  		h := make(textproto.MIMEHeader)
   417  		h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="metadata"`))
   418  		h.Set("Content-Type", "application/json")
   419  		part, err := w.CreatePart(h)
   420  		if err != nil {
   421  			return err
   422  		}
   423  		if _, err := io.Copy(part, metadataReader); err != nil {
   424  			return err
   425  		}
   426  
   427  		for _, profileReader := range profileReaders {
   428  			h = make(textproto.MIMEHeader)
   429  			h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="profile"`))
   430  			h.Set("Content-Type", `application/x-protobuf; messageType="perftools.profiles.Profile"`)
   431  			part, err = w.CreatePart(h)
   432  			if err != nil {
   433  				return err
   434  			}
   435  			if _, err := io.Copy(part, profileReader); err != nil {
   436  				return err
   437  			}
   438  		}
   439  		return w.Close()
   440  	}
   441  	pipeR, pipeW := io.Pipe()
   442  	mpw := multipart.NewWriter(pipeW)
   443  	req.Header.Set("Content-Type", mpw.FormDataContentType())
   444  	req.Body = pipeR
   445  	go func() {
   446  		err := writeBody(mpw)
   447  		pipeW.CloseWithError(err)
   448  	}()
   449  	return t.sendProfileRequest(req)
   450  }
   451  
   452  func (t *HTTPTransport) sendProfileRequest(req *http.Request) error {
   453  	resp, err := t.Client.Do(req)
   454  	if err != nil {
   455  		return errors.Wrap(err, "sending profile request failed")
   456  	}
   457  	switch resp.StatusCode {
   458  	case http.StatusOK, http.StatusAccepted:
   459  		resp.Body.Close()
   460  		return nil
   461  	}
   462  	defer resp.Body.Close()
   463  
   464  	result := newHTTPError(resp)
   465  	if resp.StatusCode == http.StatusNotFound && result.Message == "404 page not found" {
   466  		// TODO(axw) correct minimum server version.
   467  		result.Message = fmt.Sprintf("%s not found (requires APM Server 7.5.0 or newer)", req.URL)
   468  	}
   469  	return result
   470  }
   471  
   472  // WatchConfig polls the APM Server for agent config changes, sending
   473  // them over the returned channel.
   474  func (t *HTTPTransport) WatchConfig(ctx context.Context, args apmconfig.WatchParams) <-chan apmconfig.Change {
   475  	changes := make(chan apmconfig.Change)
   476  	go func() {
   477  		defer close(changes)
   478  
   479  		var etag string
   480  		var out chan apmconfig.Change
   481  		var change apmconfig.Change
   482  		timer := time.NewTimer(0)
   483  		for {
   484  			select {
   485  			case <-ctx.Done():
   486  				return
   487  			case out <- change:
   488  				out = nil
   489  				change = apmconfig.Change{}
   490  				continue
   491  			case <-timer.C:
   492  			}
   493  
   494  			urlIndex := atomic.LoadInt32(&t.urlIndex)
   495  			query := make(url.Values)
   496  			query.Set("service.name", args.Service.Name)
   497  			if args.Service.Environment != "" {
   498  				query.Set("service.environment", args.Service.Environment)
   499  			}
   500  			url := *t.configURLs[urlIndex]
   501  			url.RawQuery = query.Encode()
   502  
   503  			req := t.newRequest("GET", &url)
   504  			req.Header = t.configHeaders
   505  			if etag != "" {
   506  				req.Header = copyHeaders(req.Header)
   507  				req.Header.Set("If-None-Match", strconv.QuoteToASCII(etag))
   508  			}
   509  
   510  			req = requestWithContext(ctx, req)
   511  			resp := t.configRequest(req)
   512  			var send bool
   513  			if resp.err != nil {
   514  				// The request will have failed if the context has been
   515  				// cancelled. No need to send a a change in this case.
   516  				send = ctx.Err() == nil
   517  			}
   518  			if !send && resp.attrs != nil {
   519  				etag = resp.etag
   520  				send = true
   521  			}
   522  			if send {
   523  				change = apmconfig.Change{Err: resp.err, Attrs: resp.attrs}
   524  				out = changes
   525  			}
   526  			timer.Reset(resp.maxAge)
   527  		}
   528  	}()
   529  	return changes
   530  }
   531  
   532  func (t *HTTPTransport) configRequest(req *http.Request) configResponse {
   533  	// defaultMaxAge is the default amount of time to wait between
   534  	// requests. This should only be used when the server does not
   535  	// respond with a Cache-Control header, or where the header is
   536  	// malformed.
   537  	const defaultMaxAge = 5 * time.Minute
   538  
   539  	resp, err := t.Client.Do(req)
   540  	if err != nil {
   541  		// TODO(axw) this might indicate that the APM Server is unavailable.
   542  		// In this case, we should allow a change in URL due to SendStream
   543  		// to cut the defaultMaxAge delay short.
   544  		return configResponse{
   545  			err:    errors.Wrap(err, "sending config request failed"),
   546  			maxAge: defaultMaxAge,
   547  		}
   548  	}
   549  	defer resp.Body.Close()
   550  
   551  	var response configResponse
   552  	if etag, err := strconv.Unquote(resp.Header.Get("Etag")); err == nil {
   553  		response.etag = etag
   554  	}
   555  	cacheControl := parseCacheControl(resp.Header.Get("Cache-Control"))
   556  	response.maxAge = cacheControl.maxAge
   557  	if response.maxAge < 0 {
   558  		response.maxAge = defaultMaxAge
   559  	}
   560  
   561  	switch resp.StatusCode {
   562  	case http.StatusNotModified, http.StatusForbidden, http.StatusNotFound:
   563  		// 304 (Not Modified) is returned when the config has not changed since the previous query.
   564  		// 403 (Forbidden) is returned if the server does not have the connection to Kibana enabled.
   565  		// 404 (Not Found) is returned by old servers that do not implement the config endpoint.
   566  		return response
   567  	case http.StatusOK:
   568  		attrs := make(map[string]string)
   569  		// TODO(axw) handling EOF shouldn't be necessary, server currently responds with an empty
   570  		// body when there is no config.
   571  		if err := json.NewDecoder(resp.Body).Decode(&attrs); err != nil && err != io.EOF {
   572  			response.err = err
   573  		} else {
   574  			response.attrs = attrs
   575  		}
   576  		return response
   577  	}
   578  	response.err = newHTTPError(resp)
   579  	if response.maxAge < 5*time.Second {
   580  		response.maxAge = 5 * time.Second
   581  	}
   582  	return response
   583  }
   584  
   585  // serverInfo represents the APM Server information as exposed in the `/`
   586  // endpoint. Not all fields may be modeled in this structure.
   587  type serverInfo struct {
   588  	// Version holds the APM Server version.
   589  	Version string `json:"version,omitempty"`
   590  }
   591  
   592  // MajorServerVersion returns the APM Server's major version. When refreshStale
   593  // is true` it will request the remote APM Server's version from `/`, otherwise
   594  // it will return the cached version. If the returned first argument is 0, the
   595  // cache is stale.
   596  func (t *HTTPTransport) MajorServerVersion(ctx context.Context, refreshStale bool) uint32 {
   597  	if v := atomic.LoadUint32(&t.majorServerVersion); v > 0 || !refreshStale {
   598  		return v
   599  	}
   600  	return t.refreshMajorServerVersion(ctx)
   601  }
   602  
   603  // RefreshVersion queries the "active" remote APM Server and caches the result
   604  // locally when the operation succeeds.
   605  func (t *HTTPTransport) refreshMajorServerVersion(ctx context.Context) uint32 {
   606  	srvURL := t.intakeURLs[atomic.LoadInt32(&t.urlIndex)]
   607  	u := *srvURL
   608  	u.Path, u.RawPath = "", ""
   609  	req := requestWithContext(ctx, t.newRequest("GET", urlWithPath(&u, "/")))
   610  	req.Header = t.rootHeaders
   611  	res, err := t.Client.Do(req)
   612  	if err != nil {
   613  		return 0
   614  	}
   615  	defer res.Body.Close()
   616  
   617  	var resp serverInfo
   618  	if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
   619  		return 0
   620  	}
   621  
   622  	if resp.Version != "" {
   623  		if v, ok := parseMajorVersion(resp.Version); ok {
   624  			atomic.StoreUint32(&t.majorServerVersion, v)
   625  			return v
   626  		}
   627  	}
   628  	return atomic.LoadUint32(&t.majorServerVersion)
   629  }
   630  
   631  func (t *HTTPTransport) newRequest(method string, url *url.URL) *http.Request {
   632  	req := &http.Request{
   633  		Method:     method,
   634  		URL:        url,
   635  		Proto:      "HTTP/1.1",
   636  		ProtoMajor: 1,
   637  		ProtoMinor: 1,
   638  		Host:       url.Host,
   639  	}
   640  	return req
   641  }
   642  
   643  func urlWithPath(url *url.URL, p string) *url.URL {
   644  	urlCopy := *url
   645  	urlCopy.Path = path.Clean(urlCopy.Path + p)
   646  	if urlCopy.RawPath != "" {
   647  		urlCopy.RawPath = path.Clean(urlCopy.RawPath + p)
   648  	}
   649  	return &urlCopy
   650  }
   651  
   652  // HTTPError is an error returned by HTTPTransport methods when requests fail.
   653  type HTTPError struct {
   654  	Response *http.Response
   655  	Message  string
   656  }
   657  
   658  func newHTTPError(resp *http.Response) *HTTPError {
   659  	bodyContents, err := ioutil.ReadAll(resp.Body)
   660  	if err == nil {
   661  		resp.Body = ioutil.NopCloser(bytes.NewReader(bodyContents))
   662  	}
   663  	return &HTTPError{
   664  		Response: resp,
   665  		Message:  strings.TrimSpace(string(bodyContents)),
   666  	}
   667  }
   668  
   669  func (e *HTTPError) Error() string {
   670  	msg := fmt.Sprintf("request failed with %s", e.Response.Status)
   671  	if e.Message != "" {
   672  		msg += ": " + e.Message
   673  	}
   674  	return msg
   675  }
   676  
   677  // initServerURLs parses ELASTIC_APM_SERVER_URLS if specified,
   678  // otherwise parses ELASTIC_APM_SERVER_URL if specified. If
   679  // neither are specified, then the default localhost URL is
   680  // returned.
   681  func initServerURLs() ([]*url.URL, error) {
   682  	key := envServerURLs
   683  	value := os.Getenv(key)
   684  	if value == "" {
   685  		key = envServerURL
   686  		value = os.Getenv(key)
   687  	}
   688  	var urls []*url.URL
   689  	for _, field := range strings.Split(value, ",") {
   690  		field = strings.TrimSpace(field)
   691  		if field == "" {
   692  			continue
   693  		}
   694  		u, err := url.Parse(field)
   695  		if err != nil {
   696  			return nil, errors.Wrapf(err, "failed to parse %s", key)
   697  		}
   698  		urls = append(urls, u)
   699  	}
   700  	if len(urls) == 0 {
   701  		urls = []*url.URL{defaultServerURL}
   702  	}
   703  	return urls, nil
   704  }
   705  
   706  func requestWithContext(ctx context.Context, req *http.Request) *http.Request {
   707  	url := req.URL
   708  	req.URL = nil
   709  	reqCopy := req.WithContext(ctx)
   710  	reqCopy.URL = url
   711  	req.URL = url
   712  	return reqCopy
   713  }
   714  
   715  func loadCertificate(path string) (*x509.Certificate, error) {
   716  	pemBytes, err := ioutil.ReadFile(path)
   717  	if err != nil {
   718  		return nil, err
   719  	}
   720  	for {
   721  		var certBlock *pem.Block
   722  		certBlock, pemBytes = pem.Decode(pemBytes)
   723  		if certBlock == nil {
   724  			return nil, errors.New("missing or invalid certificate")
   725  		}
   726  		if certBlock.Type == "CERTIFICATE" {
   727  			return x509.ParseCertificate(certBlock.Bytes)
   728  		}
   729  	}
   730  }
   731  
   732  func verifyPeerCertificate(rawCerts [][]byte, trusted *x509.Certificate) error {
   733  	if len(rawCerts) == 0 {
   734  		return errors.New("missing leaf certificate")
   735  	}
   736  	cert, err := x509.ParseCertificate(rawCerts[0])
   737  	if err != nil {
   738  		return errors.Wrap(err, "failed to parse certificate from server")
   739  	}
   740  	if !cert.Equal(trusted) {
   741  		return errors.New("failed to verify server certificate")
   742  	}
   743  	return nil
   744  }
   745  
   746  // DefaultUserAgent returns the default value to use for the User-Agent header:
   747  // apm-agent-go/<agent-version>.
   748  func DefaultUserAgent() string {
   749  	return fmt.Sprintf("apm-agent-go/%s", apmversion.AgentVersion)
   750  }
   751  
   752  func copyHeaders(in http.Header) http.Header {
   753  	out := make(http.Header, len(in))
   754  	for k, vs := range in {
   755  		vsCopy := make([]string, len(vs))
   756  		copy(vsCopy, vs)
   757  		out[k] = vsCopy
   758  	}
   759  	return out
   760  }
   761  
   762  type configResponse struct {
   763  	err    error
   764  	attrs  map[string]string
   765  	etag   string
   766  	maxAge time.Duration
   767  }
   768  
   769  type cacheControl struct {
   770  	maxAge time.Duration
   771  }
   772  
   773  func parseCacheControl(s string) cacheControl {
   774  	fields := strings.SplitN(s, "max-age=", 2)
   775  	if len(fields) < 2 {
   776  		return cacheControl{maxAge: -1}
   777  	}
   778  	s = fields[1]
   779  	if i := strings.IndexRune(s, ','); i != -1 {
   780  		s = s[:i]
   781  	}
   782  	maxAge, err := strconv.ParseUint(s, 10, 32)
   783  	if err != nil {
   784  		return cacheControl{maxAge: -1}
   785  	}
   786  	return cacheControl{maxAge: time.Duration(maxAge) * time.Second}
   787  }
   788  
   789  // parseMajorVersion returns the major version given a version string. Accepts
   790  // the string as long as it contans a `.` and the runes preceding `.` can be
   791  // parsed to a number. If the operation succeeded, the second return value will
   792  // be true.
   793  func parseMajorVersion(v string) (uint32, bool) {
   794  	i := strings.IndexRune(v, '.')
   795  	if i == -1 {
   796  		return 0, false
   797  	}
   798  
   799  	major, err := strconv.Atoi(v[:i])
   800  	if err != nil {
   801  		return 0, false
   802  	}
   803  	return uint32(major), true
   804  }