blob: 24a187b500d634289317b3ba566470418cfb7794 [file] [log] [blame] [edit]
// This binary can be used to serve demo pages during development. It also serves as the test_on_env
// environment for Puppeteer tests.
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
const (
envPortFileBaseName = "port"
)
// A simple ResponseWriter wrapper that captures the response from the reverse proxy and prints its reponses for inspections.
type ResponseWriterWrapper struct {
w *http.ResponseWriter
body *bytes.Buffer
statusCode *int
}
// NewResponseWriterWrapper static function creates a wrapper for the http.ResponseWriter
func NewResponseWriterWrapper(w http.ResponseWriter) ResponseWriterWrapper {
var buf bytes.Buffer
var statusCode int = 200
return ResponseWriterWrapper{
w: &w,
body: &buf,
statusCode: &statusCode,
}
}
func (rww ResponseWriterWrapper) Write(buf []byte) (int, error) {
rww.body.Write(buf)
return (*rww.w).Write(buf)
}
// Header function overwrites the http.ResponseWriter Header() function
func (rww ResponseWriterWrapper) Header() http.Header {
return (*rww.w).Header()
}
// WriteHeader function overwrites the http.ResponseWriter WriteHeader() function
func (rww ResponseWriterWrapper) WriteHeader(statusCode int) {
(*rww.statusCode) = statusCode
(*rww.w).WriteHeader(statusCode)
}
func (rww ResponseWriterWrapper) String() string {
var buf bytes.Buffer
buf.WriteString("Response:\n")
buf.WriteString(fmt.Sprintf(" Status Code: %d\n", *(rww.statusCode)))
buf.WriteString("Headers:\n")
for k, v := range (*rww.w).Header() {
buf.WriteString(fmt.Sprintf("%s: %v\n", k, v))
}
buf.WriteString("Body:\n")
buf.WriteString(rww.body.String())
return buf.String()
}
func proxyHandler(remote *url.URL, p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL, r.Method)
log.Println(r.Header)
// Only do plain test response so we can inspect the content easily.
r.Header.Del("Accept-Encoding")
bbytes, _ := io.ReadAll(r.Body)
body := string(bbytes)
log.Println(body)
rw := NewResponseWriterWrapper(w)
nb := io.NopCloser(strings.NewReader(body))
r.Host = remote.Host
r.Body = nb
p.ServeHTTP(rw, r)
log.Println(rw.String())
}
}
func main() {
var (
assetsDir = flag.String("directory", "", "Path to directory to serve.")
port = flag.Int("port", 0, "TCP port (ignored / chosen by the OS if $ENV_DIR is set; see the test_on_env Bazel rule).")
)
flag.Parse()
// The ENV_DIR and ENV_READY_FILE environment variables are set by the test_on_env runner script.
// We detect whether we are running inside a test_on_env target based on said variables.
envDir := os.Getenv("ENV_DIR")
envReadyFile := os.Getenv("ENV_READY_FILE")
testOnEnv := envDir != "" && envReadyFile != ""
// If we're running inside a test_on_env target then we ignore the --port flag and let the OS
// choose a TCP port number for us. This prevents tests from failing with "address already in use"
// errors due to Bazel running multiple test targets in parallel.
if testOnEnv {
*port = 0
}
var (
listener net.Listener
remote *url.URL
actualPort int
err error
)
// Provide an optional staging or prod endpoint that the the demo server reverse-proxied to.
// e.g. https://perf.luci.app, or https://v8-perf.skia.org
// Because there is no redirection or login, this needs to be publicly available.
envRemoteEndpoint := os.Getenv("ENV_REMOTE_ENDPOINT")
if envRemoteEndpoint != "" {
if remote, err = url.Parse(envRemoteEndpoint); err != nil {
panic(err)
}
}
// If the port is unspecified (i.e. 0), an unused port will be chosen by the OS.
if *port == 0 {
listener, err = net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
panic(err)
}
actualPort = listener.Addr().(*net.TCPAddr).Port // Retrieve the port number chosen by the OS.
} else {
// Try opening the specified port, or repeatedly increase it by 1 until an unused port is found.
// This allows developers to view multiple demo pages at the same time.
actualPort = *port
for {
if actualPort > 65535 {
panic("no unused TCP ports found")
}
listener, err = net.Listen("tcp", fmt.Sprintf(":%d", actualPort))
if err != nil {
actualPort++
} else {
break
}
}
}
// Set up the HTTP server.
assetsDirAbs, err := filepath.Abs(*assetsDir)
if err != nil {
panic(err)
}
fmt.Printf("Serving directory %s\n", assetsDirAbs)
fileServer := http.FileServer(http.Dir(*assetsDir))
if remote != nil {
fmt.Printf("Requests are reverse-proxied to: %s\n", remote.Hostname())
proxy := httputil.NewSingleHostReverseProxy(remote)
http.HandleFunc("/_/", proxyHandler(remote, proxy))
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Session cookie used by demo pages to determine whether they are being served by
// webpack-dev-server or by an sk_demo_page_server Bazel rule.
// TODO(lovisolo): Delete.
http.SetCookie(w, &http.Cookie{
Name: "bazel",
Value: "true",
})
if remote != nil {
// This allows the frontend to skip mock.
http.SetCookie(w, &http.Cookie{
Name: "proxy_endpoint",
Value: remote.Hostname(),
})
}
fileServer.ServeHTTP(w, r)
})
// Build the demo page URL.
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
url := fmt.Sprintf("http://%s:%d", hostname, actualPort)
// We need to signal the test_on_env runner script that we are ready to accept connections.
if testOnEnv {
// First, we write out the TCP port number. This will be read by the test target.
envPortFile := path.Join(envDir, envPortFileBaseName)
err = os.WriteFile(envPortFile, []byte(strconv.Itoa(actualPort)), 0644)
if err != nil {
panic(err)
}
// Then, we write the ready file. This signals the test_on_env runner script that we are ready.
err = os.WriteFile(envReadyFile, []byte{}, 0644)
if err != nil {
panic(err)
}
}
// Start the server immediately after writing the ready file.
fmt.Printf("Serving demo page at: %s/\n", url)
err = http.Serve(listener, nil) // Always returns a non-nil error.
if err.Error() != "" {
panic(err)
}
}