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 }