Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/proxy/plugins/analytics/analytics.go
2500 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package analytics
6
7
import (
8
"log"
9
"net"
10
"net/http"
11
"net/http/httputil"
12
"net/url"
13
"os"
14
"time"
15
16
"github.com/caddyserver/caddy/v2"
17
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
18
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
19
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
20
"go.uber.org/zap"
21
22
segment "gopkg.in/segmentio/analytics-go.v3"
23
)
24
25
const (
26
moduleName = "gitpod.analytics"
27
// static key for untrusted segment requests
28
dummyUntrustedSegmentKey = "untrusted-dummy-key"
29
)
30
31
func init() {
32
caddy.RegisterModule(Analytics{})
33
httpcaddyfile.RegisterHandlerDirective(moduleName, parseCaddyfile)
34
}
35
36
type Analytics struct {
37
segmentProxy http.Handler
38
trustedSegmentKey string
39
untrustedSegmentKey string
40
}
41
42
// CaddyModule returns the Caddy module information.
43
func (Analytics) CaddyModule() caddy.ModuleInfo {
44
return caddy.ModuleInfo{
45
ID: "http.handlers.gitpod_analytics",
46
New: func() caddy.Module { return new(Analytics) },
47
}
48
}
49
50
// Provision implements caddy.Provisioner.
51
func (a *Analytics) Provision(ctx caddy.Context) error {
52
logger := ctx.Logger(a)
53
54
segmentEndpoint, err := resolveSegmenEndpoint()
55
if err != nil {
56
logger.Error("failed to parse segment endpoint", zap.Error(err))
57
return nil
58
}
59
60
errorLog, err := zap.NewStdLogAt(logger, zap.ErrorLevel)
61
if err != nil {
62
logger.Error("failed to create error log", zap.Error(err))
63
return nil
64
}
65
66
a.segmentProxy = newSegmentProxy(segmentEndpoint, errorLog)
67
a.untrustedSegmentKey = os.Getenv("ANALYTICS_PLUGIN_UNTRUSTED_SEGMENT_KEY")
68
a.trustedSegmentKey = os.Getenv("ANALYTICS_PLUGIN_TRUSTED_SEGMENT_KEY")
69
70
return nil
71
}
72
73
func resolveSegmenEndpoint() (*url.URL, error) {
74
segmentEndpoint := os.Getenv("ANALYTICS_PLUGIN_SEGMENT_ENDPOINT")
75
if segmentEndpoint == "" {
76
segmentEndpoint = segment.DefaultEndpoint
77
}
78
return url.Parse(segmentEndpoint)
79
}
80
81
func newSegmentProxy(segmentEndpoint *url.URL, errorLog *log.Logger) http.Handler {
82
reverseProxy := httputil.NewSingleHostReverseProxy(segmentEndpoint)
83
reverseProxy.ErrorLog = errorLog
84
reverseProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
85
// the default error handler is:
86
// func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
87
// p.logf("http: proxy error: %v", err)
88
// rw.WriteHeader(http.StatusBadGateway)
89
// }
90
//
91
// proxy returns 502 to clients when supervisor is having trouble, which is a signal the user experience is degraded
92
//
93
// this change makes it so that we return 503 when there is trouble with the /analytics endpoint
94
reverseProxy.ErrorLog.Printf("http: proxy error: %v", err)
95
w.WriteHeader(http.StatusServiceUnavailable)
96
}
97
98
// configure transport to ensure that requests
99
// can be processed without staling connections
100
reverseProxy.Transport = &http.Transport{
101
Proxy: http.ProxyFromEnvironment,
102
DialContext: (&net.Dialer{
103
Timeout: 30 * time.Second,
104
KeepAlive: 30 * time.Second,
105
}).DialContext,
106
MaxIdleConns: 0,
107
MaxIdleConnsPerHost: 100,
108
IdleConnTimeout: 30 * time.Second,
109
TLSHandshakeTimeout: 10 * time.Second,
110
ExpectContinueTimeout: 1 * time.Second,
111
}
112
return http.StripPrefix("/analytics", reverseProxy)
113
}
114
115
func (a *Analytics) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
116
segmentKey, _, _ := r.BasicAuth()
117
shouldProxyToTrustedSegment := a.trustedSegmentKey != "" && segmentKey == a.trustedSegmentKey
118
if shouldProxyToTrustedSegment {
119
a.segmentProxy.ServeHTTP(w, r)
120
return nil
121
}
122
shouldProxyToUntrustedSegment := a.untrustedSegmentKey != "" && (segmentKey == "" || segmentKey == dummyUntrustedSegmentKey)
123
if shouldProxyToUntrustedSegment {
124
r.SetBasicAuth(a.untrustedSegmentKey, "")
125
a.segmentProxy.ServeHTTP(w, r)
126
return nil
127
}
128
return next.ServeHTTP(w, r)
129
}
130
131
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
132
func (m *Analytics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
133
return nil
134
}
135
136
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
137
m := new(Analytics)
138
err := m.UnmarshalCaddyfile(h.Dispenser)
139
if err != nil {
140
return nil, err
141
}
142
143
return m, nil
144
}
145
146
// Interface guards
147
var (
148
_ caddyhttp.MiddlewareHandler = (*Analytics)(nil)
149
_ caddyfile.Unmarshaler = (*Analytics)(nil)
150
)
151
152