Path: blob/main/components/proxy/plugins/headlesslogdownload/headless_log_download.go
2501 views
// Copyright (c) 2021 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 workspacedownload56import (7"fmt"8"io"9"net/http"10"time"1112"github.com/caddyserver/caddy/v2"13"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"14"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"15"github.com/caddyserver/caddy/v2/modules/caddyhttp"16)1718const (19headlessLogDownloadModule = "gitpod.headless_log_download"20)2122func init() {23caddy.RegisterModule(HeadlessLogDownload{})24httpcaddyfile.RegisterHandlerDirective(headlessLogDownloadModule, parseCaddyfile)25}2627// HeadlessLogDownload implements an HTTP handler that proxies headless log downloads28// with security headers to prevent XSS attacks from malicious branch names in logs.29type HeadlessLogDownload struct {30Service string `json:"service,omitempty"`31}3233// CaddyModule returns the Caddy module information.34func (HeadlessLogDownload) CaddyModule() caddy.ModuleInfo {35return caddy.ModuleInfo{36ID: "http.handlers.gitpod_headless_log_download",37New: func() caddy.Module { return new(HeadlessLogDownload) },38}39}4041// ServeHTTP implements caddyhttp.MiddlewareHandler.42func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {43query := r.URL.RawQuery44if query != "" {45query = "?" + query46}4748// server has an endpoint on the same path that returns the upstream endpoint for the actual download49origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)50u := fmt.Sprintf("%v%v%v", m.Service, origReq.URL.Path, query)51client := http.Client{Timeout: 5 * time.Second}52req, err := http.NewRequest("GET", u, nil)53if err != nil {54caddy.Log().Sugar().Errorf("cannot resolve headless log URL %v: %w", u, err)55return fmt.Errorf("server error: cannot resolve headless log URL")56}5758// pass browser headers59// TODO (aledbf): check if it's possible to narrow the list60for k, vv := range r.Header {61for _, v := range vv {62req.Header.Add(k, v)63}64}6566// query server and parse response67resp, err := client.Do(req)68if err != nil {69return fmt.Errorf("server error: cannot resolve headless log URL")70}71defer resp.Body.Close()7273if resp.StatusCode != http.StatusOK {74return fmt.Errorf("Bad Request: /headless-log-download/get returned with code %v", resp.StatusCode)75}7677upstreamURLBytes, err := io.ReadAll(resp.Body)78if err != nil {79return fmt.Errorf("server error: cannot obtain headless log redirect URL")80}81upstreamURL := string(upstreamURLBytes)8283// perform the upstream request here84resp, err = http.Get(upstreamURL)85if err != nil {86caddy.Log().Sugar().Errorf("error starting download of prebuild log for %v: %v", upstreamURL, err)87return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))88}89defer resp.Body.Close()9091if resp.StatusCode != http.StatusOK {92caddy.Log().Sugar().Errorf("invalid status code downloading prebuild log for %v: %v", upstreamURL, resp.StatusCode)93return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))94}9596setSecurityHeaders(w)97copyResponseHeaders(w, resp)9899brw := newNoBufferResponseWriter(w)100_, err = io.Copy(brw, resp.Body)101if err != nil {102caddy.Log().Sugar().Errorf("error proxying prebuild log download for %v: %v", upstreamURL, err)103return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))104}105106return next.ServeHTTP(w, r)107}108109func setSecurityHeaders(w http.ResponseWriter) {110headers := w.Header()111headers.Set("Content-Type", "text/plain; charset=utf-8")112headers.Set("X-Content-Type-Options", "nosniff")113headers.Set("X-Frame-Options", "DENY")114headers.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")115headers.Set("Referrer-Policy", "strict-origin-when-cross-origin")116headers.Set("Cache-Control", "no-cache, no-store, must-revalidate")117}118119// copyResponseHeaders copies safe headers from upstream response, excluding potentially dangerous ones120func copyResponseHeaders(w http.ResponseWriter, resp *http.Response) {121// List of safe headers to copy from upstream122safeHeaders := []string{123"Content-Length",124"Content-Encoding",125"Content-Disposition",126"Last-Modified",127"ETag",128}129130destHeaders := w.Header()131for _, header := range safeHeaders {132if value := resp.Header.Get(header); value != "" {133destHeaders.Set(header, value)134}135}136137// Note: We intentionally do NOT copy Content-Type from upstream138// because we want to enforce text/plain for security139}140141// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.142func (m *HeadlessLogDownload) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {143if !d.Next() {144return d.Err("expected token following filter")145}146147for d.NextBlock(0) {148key := d.Val()149var value string150d.Args(&value)151if d.NextArg() {152return d.ArgErr()153}154155switch key {156case "service":157m.Service = value158default:159return d.Errf("unrecognized subdirective '%s'", d.Val())160}161}162163if m.Service == "" {164return fmt.Errorf("Please configure the service subdirective")165}166167return nil168}169170func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {171m := new(HeadlessLogDownload)172err := m.UnmarshalCaddyfile(h.Dispenser)173if err != nil {174return nil, err175}176177return m, nil178}179180// Interface guards181var (182_ caddyhttp.MiddlewareHandler = (*HeadlessLogDownload)(nil)183_ caddyfile.Unmarshaler = (*HeadlessLogDownload)(nil)184)185186// noBufferWriter ResponseWriter that allow an HTTP handler to flush buffered data to the client.187type noBufferWriter struct {188w http.ResponseWriter189flusher http.Flusher190}191192func newNoBufferResponseWriter(w http.ResponseWriter) *noBufferWriter {193writer := &noBufferWriter{194w: w,195}196if flusher, ok := w.(http.Flusher); ok {197writer.flusher = flusher198}199return writer200}201202func (n *noBufferWriter) Write(p []byte) (written int, err error) {203written, err = n.w.Write(p)204if n.flusher != nil {205n.flusher.Flush()206}207208return209}210211func (n *noBufferWriter) Header() http.Header {212return n.w.Header()213}214215func (n *noBufferWriter) WriteHeader(statusCode int) {216n.w.WriteHeader(statusCode)217}218219220