Path: blob/main/components/proxy/plugins/frontend_dev/frontend_dev.go
2500 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 frontend_dev56import (7"bytes"8"fmt"9"io"10"net/http"11"net/http/httputil"12"net/url"13"os"14"regexp"15"strings"1617"github.com/caddyserver/caddy/v2"18"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"19"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"20"github.com/caddyserver/caddy/v2/modules/caddyhttp"21)2223const (24frontendDevModule = "gitpod.frontend_dev"25devURLHeaderName = "X-Frontend-Dev-URL"26frontendDevEnabledEnvVarName = "FRONTEND_DEV_ENABLED"27)2829func init() {30caddy.RegisterModule(FrontendDev{})31httpcaddyfile.RegisterHandlerDirective(frontendDevModule, parseCaddyfile)32}3334// FrontendDev implements an HTTP handler that extracts gitpod headers35type FrontendDev struct {36Upstream string `json:"upstream,omitempty"`37UpstreamUrl *url.URL38}3940// CaddyModule returns the Caddy module information.41func (FrontendDev) CaddyModule() caddy.ModuleInfo {42return caddy.ModuleInfo{43ID: "http.handlers.frontend_dev",44New: func() caddy.Module { return new(FrontendDev) },45}46}4748// ServeHTTP implements caddyhttp.MiddlewareHandler.49func (m FrontendDev) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {50enabled := os.Getenv(frontendDevEnabledEnvVarName)51if enabled != "true" {52caddy.Log().Sugar().Debugf("Dev URL header present but disabled")53return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("frontend dev module disabled"))54}5556devURLStr := r.Header.Get(devURLHeaderName)57if devURLStr == "" {58caddy.Log().Sugar().Errorf("Dev URL header empty")59return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))60}61devURL, err := url.Parse(devURLStr)62if err != nil {63caddy.Log().Sugar().Errorf("Cannot parse dev URL")64return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))65}6667director := func(req *http.Request) {68req.URL.Scheme = m.UpstreamUrl.Scheme69req.URL.Host = m.UpstreamUrl.Host70req.Host = m.UpstreamUrl.Host71if _, ok := req.Header["User-Agent"]; !ok {72// explicitly disable User-Agent so it's not set to default value73req.Header.Set("User-Agent", "")74}75req.Header.Set("Accept-Encoding", "") // we can't handle other than plain text76// caddy.Log().Sugar().Infof("director request (mod): %v", req.URL.String())77}78proxy := httputil.ReverseProxy{Director: director, Transport: &RedirectingTransport{baseUrl: devURL}}79proxy.ServeHTTP(w, r)8081return nil82}8384type RedirectingTransport struct {85baseUrl *url.URL86}8788func (rt *RedirectingTransport) RoundTrip(req *http.Request) (*http.Response, error) {89// caddy.Log().Sugar().Infof("issuing upstream request: %s", req.URL.Path)90resp, err := http.DefaultTransport.RoundTrip(req)91if err != nil {92return nil, err93}9495// gpl: Do we have better means to avoid checking the body?96if resp.StatusCode < 300 && strings.HasPrefix(resp.Header.Get("Content-type"), "text/html") {97// caddy.Log().Sugar().Infof("trying to match request: %s", req.URL.Path)98modifiedResp := MatchAndRewriteRootRequest(resp, rt.baseUrl)99if modifiedResp != nil {100caddy.Log().Sugar().Debugf("using modified upstream response: %s", req.URL.Path)101return modifiedResp, nil102}103}104caddy.Log().Sugar().Debugf("forwarding upstream response: %s", req.URL.Path)105106return resp, nil107}108109func MatchAndRewriteRootRequest(or *http.Response, baseUrl *url.URL) *http.Response {110// match index.html?111prefix := []byte("<!doctype html>")112var buf bytes.Buffer113bodyReader := io.TeeReader(or.Body, &buf)114prefixBuf := make([]byte, len(prefix))115_, err := io.ReadAtLeast(bodyReader, prefixBuf, len(prefix))116if err != nil {117caddy.Log().Sugar().Debugf("prefix match: can't read response body: %w", err)118return nil119}120if !bytes.Equal(prefix, prefixBuf) {121caddy.Log().Sugar().Debugf("prefix mismatch: %s", string(prefixBuf))122return nil123}124125caddy.Log().Sugar().Debugf("match index.html")126_, err = io.Copy(&buf, or.Body)127if err != nil {128caddy.Log().Sugar().Debugf("unable to copy response body: %w, path: %s", err, or.Request.URL.Path)129return nil130}131fullBody := buf.String()132133mainJs := regexp.MustCompile(`"[^"]+?main\.[0-9a-z]+\.js"`)134fullBody = mainJs.ReplaceAllStringFunc(fullBody, func(s string) string {135return fmt.Sprintf(`"%s/static/js/main.js"`, baseUrl.String())136})137138mainCss := regexp.MustCompile(`<link[^>]+?rel="stylesheet">`)139fullBody = mainCss.ReplaceAllString(fullBody, "")140141hrefs := regexp.MustCompile(`href="/`)142fullBody = hrefs.ReplaceAllString(fullBody, fmt.Sprintf(`href="%s/`, baseUrl.String()))143144or.Body = io.NopCloser(strings.NewReader(fullBody))145or.Header.Set("Content-Length", fmt.Sprintf("%d", len(fullBody)))146or.Header.Set("Etag", "")147return or148}149150// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.151func (m *FrontendDev) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {152if !d.Next() {153return d.Err("expected token following filter")154}155156for d.NextBlock(0) {157key := d.Val()158var value string159d.Args(&value)160if d.NextArg() {161return d.ArgErr()162}163164switch key {165case "upstream":166m.Upstream = value167168default:169return d.Errf("unrecognized subdirective '%s'", value)170}171}172173if m.Upstream == "" {174return fmt.Errorf("frontend_dev: 'upstream' config field may not be empty")175}176177upstreamURL, err := url.Parse(m.Upstream)178if err != nil {179return fmt.Errorf("frontend_dev: 'upstream' is not a valid URL: %w", err)180}181m.UpstreamUrl = upstreamURL182183return nil184}185186func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {187m := new(FrontendDev)188err := m.UnmarshalCaddyfile(h.Dispenser)189if err != nil {190return nil, err191}192193return m, nil194}195196// Interface guards197var (198_ caddyhttp.MiddlewareHandler = (*FrontendDev)(nil)199_ caddyfile.Unmarshaler = (*FrontendDev)(nil)200)201202203