Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/proxy/plugins/headlesslogdownload/headless_log_download.go
2501 views
1
// Copyright (c) 2021 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 workspacedownload
6
7
import (
8
"fmt"
9
"io"
10
"net/http"
11
"time"
12
13
"github.com/caddyserver/caddy/v2"
14
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
15
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
16
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
17
)
18
19
const (
20
headlessLogDownloadModule = "gitpod.headless_log_download"
21
)
22
23
func init() {
24
caddy.RegisterModule(HeadlessLogDownload{})
25
httpcaddyfile.RegisterHandlerDirective(headlessLogDownloadModule, parseCaddyfile)
26
}
27
28
// HeadlessLogDownload implements an HTTP handler that proxies headless log downloads
29
// with security headers to prevent XSS attacks from malicious branch names in logs.
30
type HeadlessLogDownload struct {
31
Service string `json:"service,omitempty"`
32
}
33
34
// CaddyModule returns the Caddy module information.
35
func (HeadlessLogDownload) CaddyModule() caddy.ModuleInfo {
36
return caddy.ModuleInfo{
37
ID: "http.handlers.gitpod_headless_log_download",
38
New: func() caddy.Module { return new(HeadlessLogDownload) },
39
}
40
}
41
42
// ServeHTTP implements caddyhttp.MiddlewareHandler.
43
func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
44
query := r.URL.RawQuery
45
if query != "" {
46
query = "?" + query
47
}
48
49
// server has an endpoint on the same path that returns the upstream endpoint for the actual download
50
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
51
u := fmt.Sprintf("%v%v%v", m.Service, origReq.URL.Path, query)
52
client := http.Client{Timeout: 5 * time.Second}
53
req, err := http.NewRequest("GET", u, nil)
54
if err != nil {
55
caddy.Log().Sugar().Errorf("cannot resolve headless log URL %v: %w", u, err)
56
return fmt.Errorf("server error: cannot resolve headless log URL")
57
}
58
59
// pass browser headers
60
// TODO (aledbf): check if it's possible to narrow the list
61
for k, vv := range r.Header {
62
for _, v := range vv {
63
req.Header.Add(k, v)
64
}
65
}
66
67
// query server and parse response
68
resp, err := client.Do(req)
69
if err != nil {
70
return fmt.Errorf("server error: cannot resolve headless log URL")
71
}
72
defer resp.Body.Close()
73
74
if resp.StatusCode != http.StatusOK {
75
return fmt.Errorf("Bad Request: /headless-log-download/get returned with code %v", resp.StatusCode)
76
}
77
78
upstreamURLBytes, err := io.ReadAll(resp.Body)
79
if err != nil {
80
return fmt.Errorf("server error: cannot obtain headless log redirect URL")
81
}
82
upstreamURL := string(upstreamURLBytes)
83
84
// perform the upstream request here
85
resp, err = http.Get(upstreamURL)
86
if err != nil {
87
caddy.Log().Sugar().Errorf("error starting download of prebuild log for %v: %v", upstreamURL, err)
88
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
89
}
90
defer resp.Body.Close()
91
92
if resp.StatusCode != http.StatusOK {
93
caddy.Log().Sugar().Errorf("invalid status code downloading prebuild log for %v: %v", upstreamURL, resp.StatusCode)
94
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
95
}
96
97
setSecurityHeaders(w)
98
copyResponseHeaders(w, resp)
99
100
brw := newNoBufferResponseWriter(w)
101
_, err = io.Copy(brw, resp.Body)
102
if err != nil {
103
caddy.Log().Sugar().Errorf("error proxying prebuild log download for %v: %v", upstreamURL, err)
104
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
105
}
106
107
return next.ServeHTTP(w, r)
108
}
109
110
func setSecurityHeaders(w http.ResponseWriter) {
111
headers := w.Header()
112
headers.Set("Content-Type", "text/plain; charset=utf-8")
113
headers.Set("X-Content-Type-Options", "nosniff")
114
headers.Set("X-Frame-Options", "DENY")
115
headers.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
116
headers.Set("Referrer-Policy", "strict-origin-when-cross-origin")
117
headers.Set("Cache-Control", "no-cache, no-store, must-revalidate")
118
}
119
120
// copyResponseHeaders copies safe headers from upstream response, excluding potentially dangerous ones
121
func copyResponseHeaders(w http.ResponseWriter, resp *http.Response) {
122
// List of safe headers to copy from upstream
123
safeHeaders := []string{
124
"Content-Length",
125
"Content-Encoding",
126
"Content-Disposition",
127
"Last-Modified",
128
"ETag",
129
}
130
131
destHeaders := w.Header()
132
for _, header := range safeHeaders {
133
if value := resp.Header.Get(header); value != "" {
134
destHeaders.Set(header, value)
135
}
136
}
137
138
// Note: We intentionally do NOT copy Content-Type from upstream
139
// because we want to enforce text/plain for security
140
}
141
142
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
143
func (m *HeadlessLogDownload) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
144
if !d.Next() {
145
return d.Err("expected token following filter")
146
}
147
148
for d.NextBlock(0) {
149
key := d.Val()
150
var value string
151
d.Args(&value)
152
if d.NextArg() {
153
return d.ArgErr()
154
}
155
156
switch key {
157
case "service":
158
m.Service = value
159
default:
160
return d.Errf("unrecognized subdirective '%s'", d.Val())
161
}
162
}
163
164
if m.Service == "" {
165
return fmt.Errorf("Please configure the service subdirective")
166
}
167
168
return nil
169
}
170
171
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
172
m := new(HeadlessLogDownload)
173
err := m.UnmarshalCaddyfile(h.Dispenser)
174
if err != nil {
175
return nil, err
176
}
177
178
return m, nil
179
}
180
181
// Interface guards
182
var (
183
_ caddyhttp.MiddlewareHandler = (*HeadlessLogDownload)(nil)
184
_ caddyfile.Unmarshaler = (*HeadlessLogDownload)(nil)
185
)
186
187
// noBufferWriter ResponseWriter that allow an HTTP handler to flush buffered data to the client.
188
type noBufferWriter struct {
189
w http.ResponseWriter
190
flusher http.Flusher
191
}
192
193
func newNoBufferResponseWriter(w http.ResponseWriter) *noBufferWriter {
194
writer := &noBufferWriter{
195
w: w,
196
}
197
if flusher, ok := w.(http.Flusher); ok {
198
writer.flusher = flusher
199
}
200
return writer
201
}
202
203
func (n *noBufferWriter) Write(p []byte) (written int, err error) {
204
written, err = n.w.Write(p)
205
if n.flusher != nil {
206
n.flusher.Flush()
207
}
208
209
return
210
}
211
212
func (n *noBufferWriter) Header() http.Header {
213
return n.w.Header()
214
}
215
216
func (n *noBufferWriter) WriteHeader(statusCode int) {
217
n.w.WriteHeader(statusCode)
218
}
219
220