Path: blob/main/components/proxy/plugins/analytics/analytics.go
2500 views
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package analytics56import (7"log"8"net"9"net/http"10"net/http/httputil"11"net/url"12"os"13"time"1415"github.com/caddyserver/caddy/v2"16"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"17"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"18"github.com/caddyserver/caddy/v2/modules/caddyhttp"19"go.uber.org/zap"2021segment "gopkg.in/segmentio/analytics-go.v3"22)2324const (25moduleName = "gitpod.analytics"26// static key for untrusted segment requests27dummyUntrustedSegmentKey = "untrusted-dummy-key"28)2930func init() {31caddy.RegisterModule(Analytics{})32httpcaddyfile.RegisterHandlerDirective(moduleName, parseCaddyfile)33}3435type Analytics struct {36segmentProxy http.Handler37trustedSegmentKey string38untrustedSegmentKey string39}4041// CaddyModule returns the Caddy module information.42func (Analytics) CaddyModule() caddy.ModuleInfo {43return caddy.ModuleInfo{44ID: "http.handlers.gitpod_analytics",45New: func() caddy.Module { return new(Analytics) },46}47}4849// Provision implements caddy.Provisioner.50func (a *Analytics) Provision(ctx caddy.Context) error {51logger := ctx.Logger(a)5253segmentEndpoint, err := resolveSegmenEndpoint()54if err != nil {55logger.Error("failed to parse segment endpoint", zap.Error(err))56return nil57}5859errorLog, err := zap.NewStdLogAt(logger, zap.ErrorLevel)60if err != nil {61logger.Error("failed to create error log", zap.Error(err))62return nil63}6465a.segmentProxy = newSegmentProxy(segmentEndpoint, errorLog)66a.untrustedSegmentKey = os.Getenv("ANALYTICS_PLUGIN_UNTRUSTED_SEGMENT_KEY")67a.trustedSegmentKey = os.Getenv("ANALYTICS_PLUGIN_TRUSTED_SEGMENT_KEY")6869return nil70}7172func resolveSegmenEndpoint() (*url.URL, error) {73segmentEndpoint := os.Getenv("ANALYTICS_PLUGIN_SEGMENT_ENDPOINT")74if segmentEndpoint == "" {75segmentEndpoint = segment.DefaultEndpoint76}77return url.Parse(segmentEndpoint)78}7980func newSegmentProxy(segmentEndpoint *url.URL, errorLog *log.Logger) http.Handler {81reverseProxy := httputil.NewSingleHostReverseProxy(segmentEndpoint)82reverseProxy.ErrorLog = errorLog83reverseProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {84// the default error handler is:85// func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {86// p.logf("http: proxy error: %v", err)87// rw.WriteHeader(http.StatusBadGateway)88// }89//90// proxy returns 502 to clients when supervisor is having trouble, which is a signal the user experience is degraded91//92// this change makes it so that we return 503 when there is trouble with the /analytics endpoint93reverseProxy.ErrorLog.Printf("http: proxy error: %v", err)94w.WriteHeader(http.StatusServiceUnavailable)95}9697// configure transport to ensure that requests98// can be processed without staling connections99reverseProxy.Transport = &http.Transport{100Proxy: http.ProxyFromEnvironment,101DialContext: (&net.Dialer{102Timeout: 30 * time.Second,103KeepAlive: 30 * time.Second,104}).DialContext,105MaxIdleConns: 0,106MaxIdleConnsPerHost: 100,107IdleConnTimeout: 30 * time.Second,108TLSHandshakeTimeout: 10 * time.Second,109ExpectContinueTimeout: 1 * time.Second,110}111return http.StripPrefix("/analytics", reverseProxy)112}113114func (a *Analytics) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {115segmentKey, _, _ := r.BasicAuth()116shouldProxyToTrustedSegment := a.trustedSegmentKey != "" && segmentKey == a.trustedSegmentKey117if shouldProxyToTrustedSegment {118a.segmentProxy.ServeHTTP(w, r)119return nil120}121shouldProxyToUntrustedSegment := a.untrustedSegmentKey != "" && (segmentKey == "" || segmentKey == dummyUntrustedSegmentKey)122if shouldProxyToUntrustedSegment {123r.SetBasicAuth(a.untrustedSegmentKey, "")124a.segmentProxy.ServeHTTP(w, r)125return nil126}127return next.ServeHTTP(w, r)128}129130// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.131func (m *Analytics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {132return nil133}134135func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {136m := new(Analytics)137err := m.UnmarshalCaddyfile(h.Dispenser)138if err != nil {139return nil, err140}141142return m, nil143}144145// Interface guards146var (147_ caddyhttp.MiddlewareHandler = (*Analytics)(nil)148_ caddyfile.Unmarshaler = (*Analytics)(nil)149)150151152