Handle gzip HTML proxy injection (#876)
* Handle gzip HTML proxy injection * Avoid require in gzip handler test * Run go mod tidy
This commit is contained in:
@@ -10,6 +10,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/sys v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -27,7 +28,6 @@ require (
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
|
||||
+43
-6
@@ -2,6 +2,7 @@ package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
@@ -69,21 +70,33 @@ func (p *Proxy) BuildFailed(msg BuildFailedMsg) {
|
||||
p.stream.BuildFailed(msg)
|
||||
}
|
||||
|
||||
func (p *Proxy) injectLiveReload(resp *http.Response) (string, error) {
|
||||
func (p *Proxy) injectLiveReload(resp *http.Response) (string, bool, error) {
|
||||
reader := resp.Body
|
||||
decodedGzip := false
|
||||
if isGzipEncoded(resp.Header) {
|
||||
gzipReader, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("proxy inject: failed to init gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
reader = gzipReader
|
||||
decodedGzip = true
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
||||
return "", fmt.Errorf("proxy inject: failed to read body from http response")
|
||||
if _, err := buf.ReadFrom(reader); err != nil {
|
||||
return "", decodedGzip, fmt.Errorf("proxy inject: failed to read body from http response: %w", err)
|
||||
}
|
||||
page := buf.String()
|
||||
|
||||
// the script will be injected before the end of the body tag. In case the tag is missing, the injection will be skipped with no error.
|
||||
body := strings.LastIndex(page, "</body>")
|
||||
if body == -1 {
|
||||
return page, nil
|
||||
return page, decodedGzip, nil
|
||||
}
|
||||
|
||||
script := "<script>" + ProxyScript + "</script>"
|
||||
return page[:body] + script + page[body:], nil
|
||||
return page[:body] + script + page[body:], decodedGzip, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -194,11 +207,14 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
// HTML: inject live reload script
|
||||
page, err := p.injectLiveReload(resp)
|
||||
page, decodedGzip, err := p.injectLiveReload(resp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if decodedGzip {
|
||||
w.Header().Del("Content-Encoding")
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa((len([]byte(page)))))
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err := io.WriteString(w, page); err != nil {
|
||||
@@ -208,6 +224,27 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func isGzipEncoded(header http.Header) bool {
|
||||
encoding := header.Get("Content-Encoding")
|
||||
if encoding == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
hasGzip := false
|
||||
for _, value := range strings.Split(encoding, ",") {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(value))
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if trimmed != "gzip" && trimmed != "x-gzip" {
|
||||
return false
|
||||
}
|
||||
hasGzip = true
|
||||
}
|
||||
|
||||
return hasGzip
|
||||
}
|
||||
|
||||
func (p *Proxy) reloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, err := w.(http.Flusher)
|
||||
if !err {
|
||||
|
||||
+41
-1
@@ -2,6 +2,7 @@ package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -213,7 +214,7 @@ func TestProxy_injectLiveReload(t *testing.T) {
|
||||
ProxyPort: 1111,
|
||||
AppPort: 2222,
|
||||
})
|
||||
got, _ := proxy.injectLiveReload(tt.given)
|
||||
got, _, _ := proxy.injectLiveReload(tt.given)
|
||||
if got != tt.expect {
|
||||
// Use a more descriptive error message
|
||||
if len(got) > 100 || len(tt.expect) > 100 {
|
||||
@@ -295,6 +296,45 @@ func TestProxy_reloadHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_proxyHandler_GzipHTML(t *testing.T) {
|
||||
body := "<body><h1>gzip</h1></body>"
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gzipWriter := gzip.NewWriter(w)
|
||||
if _, err := io.WriteString(gzipWriter, body); err != nil {
|
||||
t.Errorf("write gzip body: %v", err)
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
t.Errorf("close gzip writer: %v", err)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
srvPort := getServerPort(t, srv)
|
||||
proxy := NewProxy(&cfgProxy{
|
||||
Enabled: true,
|
||||
ProxyPort: proxyPort,
|
||||
AppPort: srvPort,
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://localhost:%d/", proxyPort), nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
rec := httptest.NewRecorder()
|
||||
proxy.proxyHandler(rec, req)
|
||||
|
||||
resp := rec.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Empty(t, resp.Header.Get("Content-Encoding"))
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(responseBody), ProxyScript)
|
||||
}
|
||||
|
||||
func TestProxy_proxyHandler_SSE(t *testing.T) {
|
||||
events := []string{
|
||||
"event: message\ndata: {\"id\":1}\n\n",
|
||||
|
||||
Reference in New Issue
Block a user