mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
219 lines
6.2 KiB
Go
219 lines
6.2 KiB
Go
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License.AGPL.txt in the project root for license information.
|
|
|
|
package workspacedownload
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
const (
|
|
headlessLogDownloadModule = "gitpod.headless_log_download"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(HeadlessLogDownload{})
|
|
httpcaddyfile.RegisterHandlerDirective(headlessLogDownloadModule, parseCaddyfile)
|
|
}
|
|
|
|
// HeadlessLogDownload implements an HTTP handler that proxies headless log downloads
|
|
// with security headers to prevent XSS attacks from malicious branch names in logs.
|
|
type HeadlessLogDownload struct {
|
|
Service string `json:"service,omitempty"`
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (HeadlessLogDownload) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.handlers.gitpod_headless_log_download",
|
|
New: func() caddy.Module { return new(HeadlessLogDownload) },
|
|
}
|
|
}
|
|
|
|
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
|
func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
|
query := r.URL.RawQuery
|
|
if query != "" {
|
|
query = "?" + query
|
|
}
|
|
|
|
// server has an endpoint on the same path that returns the upstream endpoint for the actual download
|
|
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
|
u := fmt.Sprintf("%v%v%v", m.Service, origReq.URL.Path, query)
|
|
client := http.Client{Timeout: 5 * time.Second}
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
caddy.Log().Sugar().Errorf("cannot resolve headless log URL %v: %w", u, err)
|
|
return fmt.Errorf("server error: cannot resolve headless log URL")
|
|
}
|
|
|
|
// pass browser headers
|
|
// TODO (aledbf): check if it's possible to narrow the list
|
|
for k, vv := range r.Header {
|
|
for _, v := range vv {
|
|
req.Header.Add(k, v)
|
|
}
|
|
}
|
|
|
|
// query server and parse response
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("server error: cannot resolve headless log URL")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("Bad Request: /headless-log-download/get returned with code %v", resp.StatusCode)
|
|
}
|
|
|
|
upstreamURLBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("server error: cannot obtain headless log redirect URL")
|
|
}
|
|
upstreamURL := string(upstreamURLBytes)
|
|
|
|
// perform the upstream request here
|
|
resp, err = http.Get(upstreamURL)
|
|
if err != nil {
|
|
caddy.Log().Sugar().Errorf("error starting download of prebuild log for %v: %v", upstreamURL, err)
|
|
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
caddy.Log().Sugar().Errorf("invalid status code downloading prebuild log for %v: %v", upstreamURL, resp.StatusCode)
|
|
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
|
|
}
|
|
|
|
setSecurityHeaders(w)
|
|
copyResponseHeaders(w, resp)
|
|
|
|
brw := newNoBufferResponseWriter(w)
|
|
_, err = io.Copy(brw, resp.Body)
|
|
if err != nil {
|
|
caddy.Log().Sugar().Errorf("error proxying prebuild log download for %v: %v", upstreamURL, err)
|
|
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
|
|
}
|
|
|
|
return next.ServeHTTP(w, r)
|
|
}
|
|
|
|
func setSecurityHeaders(w http.ResponseWriter) {
|
|
headers := w.Header()
|
|
headers.Set("Content-Type", "text/plain; charset=utf-8")
|
|
headers.Set("X-Content-Type-Options", "nosniff")
|
|
headers.Set("X-Frame-Options", "DENY")
|
|
headers.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
|
headers.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
headers.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
}
|
|
|
|
// copyResponseHeaders copies safe headers from upstream response, excluding potentially dangerous ones
|
|
func copyResponseHeaders(w http.ResponseWriter, resp *http.Response) {
|
|
// List of safe headers to copy from upstream
|
|
safeHeaders := []string{
|
|
"Content-Length",
|
|
"Content-Encoding",
|
|
"Content-Disposition",
|
|
"Last-Modified",
|
|
"ETag",
|
|
}
|
|
|
|
destHeaders := w.Header()
|
|
for _, header := range safeHeaders {
|
|
if value := resp.Header.Get(header); value != "" {
|
|
destHeaders.Set(header, value)
|
|
}
|
|
}
|
|
|
|
// Note: We intentionally do NOT copy Content-Type from upstream
|
|
// because we want to enforce text/plain for security
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
|
|
func (m *HeadlessLogDownload) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
if !d.Next() {
|
|
return d.Err("expected token following filter")
|
|
}
|
|
|
|
for d.NextBlock(0) {
|
|
key := d.Val()
|
|
var value string
|
|
d.Args(&value)
|
|
if d.NextArg() {
|
|
return d.ArgErr()
|
|
}
|
|
|
|
switch key {
|
|
case "service":
|
|
m.Service = value
|
|
default:
|
|
return d.Errf("unrecognized subdirective '%s'", d.Val())
|
|
}
|
|
}
|
|
|
|
if m.Service == "" {
|
|
return fmt.Errorf("Please configure the service subdirective")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
m := new(HeadlessLogDownload)
|
|
err := m.UnmarshalCaddyfile(h.Dispenser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Interface guards
|
|
var (
|
|
_ caddyhttp.MiddlewareHandler = (*HeadlessLogDownload)(nil)
|
|
_ caddyfile.Unmarshaler = (*HeadlessLogDownload)(nil)
|
|
)
|
|
|
|
// noBufferWriter ResponseWriter that allow an HTTP handler to flush buffered data to the client.
|
|
type noBufferWriter struct {
|
|
w http.ResponseWriter
|
|
flusher http.Flusher
|
|
}
|
|
|
|
func newNoBufferResponseWriter(w http.ResponseWriter) *noBufferWriter {
|
|
writer := &noBufferWriter{
|
|
w: w,
|
|
}
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
writer.flusher = flusher
|
|
}
|
|
return writer
|
|
}
|
|
|
|
func (n *noBufferWriter) Write(p []byte) (written int, err error) {
|
|
written, err = n.w.Write(p)
|
|
if n.flusher != nil {
|
|
n.flusher.Flush()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (n *noBufferWriter) Header() http.Header {
|
|
return n.w.Header()
|
|
}
|
|
|
|
func (n *noBufferWriter) WriteHeader(statusCode int) {
|
|
n.w.WriteHeader(statusCode)
|
|
}
|