mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
* [dev] make-user-admin: Grant all admin permissions * [dev] Change frontend-dev to redirect instead of transparently proxy This removes the "proxy needs network access to upstream" constraint, by relying on the developers browser * [dev] front-end dev: more convenience, and added to readme
202 lines
5.9 KiB
Go
202 lines
5.9 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 frontend_dev
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"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 (
|
|
frontendDevModule = "gitpod.frontend_dev"
|
|
devURLHeaderName = "X-Frontend-Dev-URL"
|
|
frontendDevEnabledEnvVarName = "FRONTEND_DEV_ENABLED"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(FrontendDev{})
|
|
httpcaddyfile.RegisterHandlerDirective(frontendDevModule, parseCaddyfile)
|
|
}
|
|
|
|
// FrontendDev implements an HTTP handler that extracts gitpod headers
|
|
type FrontendDev struct {
|
|
Upstream string `json:"upstream,omitempty"`
|
|
UpstreamUrl *url.URL
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (FrontendDev) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.handlers.frontend_dev",
|
|
New: func() caddy.Module { return new(FrontendDev) },
|
|
}
|
|
}
|
|
|
|
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
|
func (m FrontendDev) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
|
enabled := os.Getenv(frontendDevEnabledEnvVarName)
|
|
if enabled != "true" {
|
|
caddy.Log().Sugar().Debugf("Dev URL header present but disabled")
|
|
return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("frontend dev module disabled"))
|
|
}
|
|
|
|
devURLStr := r.Header.Get(devURLHeaderName)
|
|
if devURLStr == "" {
|
|
caddy.Log().Sugar().Errorf("Dev URL header empty")
|
|
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))
|
|
}
|
|
devURL, err := url.Parse(devURLStr)
|
|
if err != nil {
|
|
caddy.Log().Sugar().Errorf("Cannot parse dev URL")
|
|
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))
|
|
}
|
|
|
|
director := func(req *http.Request) {
|
|
req.URL.Scheme = m.UpstreamUrl.Scheme
|
|
req.URL.Host = m.UpstreamUrl.Host
|
|
req.Host = m.UpstreamUrl.Host
|
|
if _, ok := req.Header["User-Agent"]; !ok {
|
|
// explicitly disable User-Agent so it's not set to default value
|
|
req.Header.Set("User-Agent", "")
|
|
}
|
|
req.Header.Set("Accept-Encoding", "") // we can't handle other than plain text
|
|
// caddy.Log().Sugar().Infof("director request (mod): %v", req.URL.String())
|
|
}
|
|
proxy := httputil.ReverseProxy{Director: director, Transport: &RedirectingTransport{baseUrl: devURL}}
|
|
proxy.ServeHTTP(w, r)
|
|
|
|
return nil
|
|
}
|
|
|
|
type RedirectingTransport struct {
|
|
baseUrl *url.URL
|
|
}
|
|
|
|
func (rt *RedirectingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// caddy.Log().Sugar().Infof("issuing upstream request: %s", req.URL.Path)
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// gpl: Do we have better means to avoid checking the body?
|
|
if resp.StatusCode < 300 && strings.HasPrefix(resp.Header.Get("Content-type"), "text/html") {
|
|
// caddy.Log().Sugar().Infof("trying to match request: %s", req.URL.Path)
|
|
modifiedResp := MatchAndRewriteRootRequest(resp, rt.baseUrl)
|
|
if modifiedResp != nil {
|
|
caddy.Log().Sugar().Debugf("using modified upstream response: %s", req.URL.Path)
|
|
return modifiedResp, nil
|
|
}
|
|
}
|
|
caddy.Log().Sugar().Debugf("forwarding upstream response: %s", req.URL.Path)
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func MatchAndRewriteRootRequest(or *http.Response, baseUrl *url.URL) *http.Response {
|
|
// match index.html?
|
|
prefix := []byte("<!doctype html>")
|
|
var buf bytes.Buffer
|
|
bodyReader := io.TeeReader(or.Body, &buf)
|
|
prefixBuf := make([]byte, len(prefix))
|
|
_, err := io.ReadAtLeast(bodyReader, prefixBuf, len(prefix))
|
|
if err != nil {
|
|
caddy.Log().Sugar().Debugf("prefix match: can't read response body: %w", err)
|
|
return nil
|
|
}
|
|
if !bytes.Equal(prefix, prefixBuf) {
|
|
caddy.Log().Sugar().Debugf("prefix mismatch: %s", string(prefixBuf))
|
|
return nil
|
|
}
|
|
|
|
caddy.Log().Sugar().Debugf("match index.html")
|
|
_, err = io.Copy(&buf, or.Body)
|
|
if err != nil {
|
|
caddy.Log().Sugar().Debugf("unable to copy response body: %w, path: %s", err, or.Request.URL.Path)
|
|
return nil
|
|
}
|
|
fullBody := buf.String()
|
|
|
|
mainJs := regexp.MustCompile(`"[^"]+?main\.[0-9a-z]+\.js"`)
|
|
fullBody = mainJs.ReplaceAllStringFunc(fullBody, func(s string) string {
|
|
return fmt.Sprintf(`"%s/static/js/main.js"`, baseUrl.String())
|
|
})
|
|
|
|
mainCss := regexp.MustCompile(`<link[^>]+?rel="stylesheet">`)
|
|
fullBody = mainCss.ReplaceAllString(fullBody, "")
|
|
|
|
hrefs := regexp.MustCompile(`href="/`)
|
|
fullBody = hrefs.ReplaceAllString(fullBody, fmt.Sprintf(`href="%s/`, baseUrl.String()))
|
|
|
|
or.Body = io.NopCloser(strings.NewReader(fullBody))
|
|
or.Header.Set("Content-Length", fmt.Sprintf("%d", len(fullBody)))
|
|
or.Header.Set("Etag", "")
|
|
return or
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
|
|
func (m *FrontendDev) 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 "upstream":
|
|
m.Upstream = value
|
|
|
|
default:
|
|
return d.Errf("unrecognized subdirective '%s'", value)
|
|
}
|
|
}
|
|
|
|
if m.Upstream == "" {
|
|
return fmt.Errorf("frontend_dev: 'upstream' config field may not be empty")
|
|
}
|
|
|
|
upstreamURL, err := url.Parse(m.Upstream)
|
|
if err != nil {
|
|
return fmt.Errorf("frontend_dev: 'upstream' is not a valid URL: %w", err)
|
|
}
|
|
m.UpstreamUrl = upstreamURL
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
m := new(FrontendDev)
|
|
err := m.UnmarshalCaddyfile(h.Dispenser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Interface guards
|
|
var (
|
|
_ caddyhttp.MiddlewareHandler = (*FrontendDev)(nil)
|
|
_ caddyfile.Unmarshaler = (*FrontendDev)(nil)
|
|
)
|