149 lines
4.8 KiB
Go
149 lines
4.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPlaybackNginxFlushesMediaAndPreservesCookies(t *testing.T) {
|
|
nginx, err := exec.LookPath("nginx")
|
|
if err != nil || runtime.GOOS == "windows" {
|
|
t.Skip("local Nginx is unavailable")
|
|
}
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.URL.Path == "/ready" {
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
cookie, err := request.Cookie("synthetic_viewer")
|
|
if err != nil || cookie.Value != "local-fixture" {
|
|
http.Error(writer, "missing fixture identity", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if request.URL.Path == "/stream" && !playbackRequestAllowed(writer, request, http.MethodGet) {
|
|
return
|
|
}
|
|
http.SetCookie(writer, &http.Cookie{Name: "synthetic_seen", Value: "yes", Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
|
writer.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = io.WriteString(writer, "synthetic-init")
|
|
writer.(http.Flusher).Flush()
|
|
select {
|
|
case <-request.Context().Done():
|
|
case <-time.After(5 * time.Second):
|
|
}
|
|
}))
|
|
defer upstream.Close()
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
address := listener.Addr().String()
|
|
_ = listener.Close()
|
|
directory := t.TempDir()
|
|
configuration := fmt.Sprintf(`worker_processes 1;
|
|
error_log stderr;
|
|
pid "%[1]s/nginx.pid";
|
|
lock_file "%[1]s/nginx.lock";
|
|
events { worker_connections 32; }
|
|
http {
|
|
access_log off;
|
|
client_body_temp_path "%[1]s/client_temp";
|
|
proxy_temp_path "%[1]s/proxy_temp";
|
|
fastcgi_temp_path "%[1]s/fastcgi_temp";
|
|
uwsgi_temp_path "%[1]s/uwsgi_temp";
|
|
scgi_temp_path "%[1]s/scgi_temp";
|
|
server {
|
|
listen %[2]s;
|
|
location / {
|
|
proxy_pass %[3]s;
|
|
proxy_set_header Host $http_host;
|
|
proxy_http_version 1.1;
|
|
proxy_buffering on;
|
|
proxy_buffer_size 4k;
|
|
proxy_buffers 8 4k;
|
|
}
|
|
}
|
|
}
|
|
`, directory, address, upstream.URL)
|
|
path := filepath.Join(directory, "nginx.conf")
|
|
if err := os.WriteFile(path, []byte(configuration), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, nginx, "-e", "stderr", "-p", directory+string(os.PathSeparator), "-c", path, "-g", "daemon off; master_process off;")
|
|
output := &cappedStringWriter{limit: 8192}
|
|
command.Stderr = output
|
|
command.WaitDelay = time.Second
|
|
if err := command.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
done := make(chan struct{})
|
|
var exitErr error
|
|
go func() { exitErr = command.Wait(); close(done) }()
|
|
defer func() { cancel(); <-done }()
|
|
client := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: time.Second}
|
|
defer client.CloseIdleConnections()
|
|
base := "http://" + address
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for {
|
|
response, err := client.Get(base + "/ready")
|
|
if err == nil {
|
|
response.Body.Close()
|
|
break
|
|
}
|
|
select {
|
|
case <-done:
|
|
if strings.Contains(output.String(), "sysctlbyname") && strings.Contains(output.String(), "Operation not permitted") {
|
|
t.Skip("local Nginx cannot read TCP settings in the sandbox; run this integration test with system access")
|
|
}
|
|
t.Fatal("Nginx fixture exited", exitErr, output.String())
|
|
default:
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("Nginx fixture did not start", err)
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
for _, route := range []string{"buffered", "stream"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
request, _ := http.NewRequest(http.MethodGet, base+"/"+route, nil)
|
|
request.Header.Set("Sec-Fetch-Site", "same-origin")
|
|
request.Header.Set("Origin", "https://library.example.invalid")
|
|
request.AddCookie(&http.Cookie{Name: "synthetic_viewer", Value: "local-fixture"})
|
|
started := time.Now()
|
|
response, err := client.Do(request)
|
|
var body []byte
|
|
if err == nil {
|
|
defer response.Body.Close()
|
|
body = make([]byte, len("synthetic-init"))
|
|
_, err = io.ReadFull(response.Body, body)
|
|
}
|
|
if route == "buffered" {
|
|
if err == nil {
|
|
t.Logf("default buffering also forwarded this fixture in %s; buffering alone does not establish a deployment failure", time.Since(started))
|
|
}
|
|
return
|
|
}
|
|
if err != nil || string(body) != "synthetic-init" || response.StatusCode != http.StatusOK {
|
|
t.Fatal("Nginx buffered or rejected media initialization", err, string(body))
|
|
}
|
|
if !strings.Contains(response.Header.Get("Cache-Control"), "no-store") || !strings.Contains(response.Header.Get("Set-Cookie"), "synthetic_seen=yes") {
|
|
t.Fatal("proxy lost privacy or browser identity headers", response.Header)
|
|
}
|
|
t.Logf("first media bytes arrived through Nginx in %s before upstream completion", time.Since(started))
|
|
})
|
|
}
|
|
}
|