package l7
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"canmi.net/vane-mock-tests/pkg/config/advanced"
"canmi.net/vane-mock-tests/pkg/env"
"canmi.net/vane-mock-tests/pkg/term"
)
func setupStaticTest(s *env.Sandbox, extraInputs map[string]interface{}) (int, string, error) {
tmpRoot := filepath.Join(s.RootDir, "static_content")
os.MkdirAll(tmpRoot, 0755)
os.WriteFile(filepath.Join(tmpRoot, "index.html"), []byte("<h1>Hello Vane</h1>"), 0644)
largeContent := strings.Repeat("ABCDEFGHIJ", 10) os.WriteFile(filepath.Join(tmpRoot, "large.txt"), []byte(largeContent), 0644)
ports, _ := env.GetFreePorts(1)
vanePort := ports[0]
inputs := map[string]interface{}{
"root": tmpRoot,
"uri": "{{req.path}}",
}
for k, v := range extraInputs {
inputs[k] = v
}
l7Conf := advanced.ApplicationConfig{
Pipeline: advanced.ProcessingStep{
"internal.driver.static": advanced.PluginInstance{
Input: inputs,
Output: map[string]advanced.ProcessingStep{
"success": {
"internal.terminator.response": advanced.PluginInstance{
Input: map[string]interface{}{},
},
},
"not_found": {
"internal.terminator.response": advanced.PluginInstance{
Input: map[string]interface{}{"status": 404, "body": "Not Found Custom"},
},
},
"failure": {
"internal.terminator.response": advanced.PluginInstance{
Input: map[string]interface{}{"status": 500, "body": "Static Error"},
},
},
},
},
},
}
l7Bytes, _ := json.Marshal(l7Conf)
s.WriteConfig("application/httpx.json", l7Bytes)
l4pConf := advanced.L4FlowConfig{Connection: advanced.NewUpgrade("httpx")}
l4pBytes, _ := json.Marshal(l4pConf)
s.WriteConfig("resolver/http.json", l4pBytes)
l4Conf := advanced.L4FlowConfig{Connection: advanced.NewUpgrade("http")}
l4Bytes, _ := json.Marshal(l4Conf)
s.WriteConfig(fmt.Sprintf("listener/[%d]/tcp.json", vanePort), l4Bytes)
return vanePort, tmpRoot, nil
}
func TestStaticServeBasic(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, _, _ := setupStaticTest(s, nil)
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", vanePort))
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "Hello Vane") {
return term.FormatFailure("Failed to serve index.html", term.NewNode(string(body)))
}
resp2, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/large.txt", vanePort))
if err != nil {
return err
}
defer resp2.Body.Close()
if resp2.StatusCode != 200 || resp2.ContentLength != 100 {
return term.FormatFailure("Failed to serve large.txt", nil)
}
return nil
}
func TestStaticRange(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, _, _ := setupStaticTest(s, nil)
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
client := &http.Client{Timeout: 2 * time.Second}
req, _ := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%d/large.txt", vanePort), nil)
req.Header.Set("Range", "bytes=10-19")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 206 {
return term.FormatFailure(fmt.Sprintf("Expected 206, got %d", resp.StatusCode), nil)
}
body, _ := io.ReadAll(resp.Body)
if len(body) != 10 {
return term.FormatFailure(fmt.Sprintf("Expected 10 bytes, got %d", len(body)), nil)
}
if string(body) != "ABCDEFGHIJ" {
return term.FormatFailure(fmt.Sprintf("Wrong range content: %s", string(body)), nil)
}
return nil
}
func TestStaticSPA(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, _, _ := setupStaticTest(s, map[string]interface{}{"spa": true})
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/some/missing/route", vanePort))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return term.FormatFailure(fmt.Sprintf("SPA: Expected 200, got %d", resp.StatusCode), nil)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "Hello Vane") {
return term.FormatFailure("SPA: Failed to fallback to index.html", term.NewNode(string(body)))
}
return nil
}
func TestStaticBrowse(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, root, _ := setupStaticTest(s, map[string]interface{}{"browse": true})
os.Remove(filepath.Join(root, "index.html"))
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", vanePort))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return term.FormatFailure(fmt.Sprintf("Browse: Expected 200, got %d", resp.StatusCode), nil)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "large.txt") || !strings.Contains(string(body), "<a href=") {
return term.FormatFailure("Browse: Listing HTML missing file links", term.NewNode(string(body)))
}
return nil
}
func TestStaticTraversal(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, _, _ := setupStaticTest(s, nil)
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/../../etc/passwd", vanePort))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return term.FormatFailure("Directory traversal succeeded (vulnerability!)", nil)
}
return nil
}
func TestStaticPrecompressed(ctx context.Context, s *env.Sandbox) error {
debug, _ := ctx.Value(env.DebugKey).(bool)
vanePort, root, _ := setupStaticTest(s, map[string]interface{}{"precompress": true})
os.WriteFile(filepath.Join(root, "style.css"), []byte("body { color: red; }"), 0644)
var b bytes.Buffer
gw := gzip.NewWriter(&b)
gw.Write([]byte("GZIP_MOCK_CONTENT"))
gw.Close()
os.WriteFile(filepath.Join(root, "style.css.gz"), b.Bytes(), 0644)
proc, err := s.StartVane(ctx, debug)
if err != nil {
return err
}
defer proc.Stop()
proc.WaitForTcpPort(vanePort, 5*time.Second)
tr := &http.Transport{
DisableCompression: true,
}
client := &http.Client{
Timeout: 2 * time.Second,
Transport: tr,
}
req, _ := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%d/style.css", vanePort), nil)
req.Header.Set("Accept-Encoding", "gzip, deflate")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return term.FormatFailure(fmt.Sprintf("Expected 200, got %d", resp.StatusCode), nil)
}
if ce := resp.Header.Get("Content-Encoding"); ce != "gzip" {
return term.FormatFailure(fmt.Sprintf("Expected Content-Encoding: gzip, got '%s'", ce), nil)
}
body, _ := io.ReadAll(resp.Body)
if !bytes.Equal(body, b.Bytes()) {
return term.FormatFailure("Did not serve correct .gz bytes", nil)
}
req2, _ := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%d/style.css", vanePort), nil)
resp2, err := client.Do(req2)
if err != nil {
return err
}
defer resp2.Body.Close()
if ce := resp2.Header.Get("Content-Encoding"); ce != "" {
return term.FormatFailure(fmt.Sprintf("Expected no Content-Encoding, got '%s'", ce), nil)
}
body2, _ := io.ReadAll(resp2.Body)
if string(body2) != "body { color: red; }" {
return term.FormatFailure("Did not serve original content", term.NewNode(string(body2)))
}
return nil
}