server_test.go raw

   1  package httpsrv
   2  
   3  import (
   4  	"bufio"
   5  	"fmt"
   6  	"io"
   7  	mrand "math/rand"
   8  	"net"
   9  	"net/http"
  10  	"strings"
  11  	"testing"
  12  	"time"
  13  )
  14  
  15  func startTestServer(t *testing.T, handler func(*Request, *ResponseWriter)) (*Server, string) {
  16  	t.Helper()
  17  	srv := &Server{Handler: handler, MaxBodyBytes: 1 << 20}
  18  	started := false
  19  	for i := 0; i < 40; i++ {
  20  		srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000))
  21  		if err := srv.Start(); err == nil {
  22  			started = true
  23  			break
  24  		}
  25  	}
  26  	if !started {
  27  		t.Fatal("could not bind a test port")
  28  	}
  29  	go srv.Serve()
  30  	// Start has already bound and listening(2) is done, so connections queue
  31  	// in the kernel backlog even before the accept goroutine is scheduled.
  32  	time.Sleep(2 * time.Millisecond)
  33  	t.Cleanup(func() { srv.Close() })
  34  	return srv, srv.Addr
  35  }
  36  
  37  func TestGetRequestParsing(t *testing.T) {
  38  	var gotMethod, gotPath, gotQuery, gotHeader, gotRemote string
  39  	_, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
  40  		gotMethod, gotPath, gotQuery = req.Method, req.Path, req.RawQuery
  41  		gotHeader = req.Header["x-test"]
  42  		gotRemote = req.RemoteAddr
  43  		w.SetHeader("content-type", "text/plain")
  44  		w.WriteHeader(200)
  45  		w.WriteString("ok")
  46  	})
  47  
  48  	req, _ := http.NewRequest("GET", "http://"+addr+"/some/path?a=b&c=d", nil)
  49  	req.Header.Set("X-Test", "value")
  50  	resp, err := http.DefaultClient.Do(req)
  51  	if err != nil {
  52  		t.Fatal(err)
  53  	}
  54  	body, _ := io.ReadAll(resp.Body)
  55  	resp.Body.Close()
  56  	if resp.StatusCode != 200 || string(body) != "ok" {
  57  		t.Fatalf("response: %d %q", resp.StatusCode, body)
  58  	}
  59  	if gotMethod != "GET" || gotPath != "/some/path" || gotQuery != "a=b&c=d" {
  60  		t.Fatalf("request line: %q %q %q", gotMethod, gotPath, gotQuery)
  61  	}
  62  	if gotHeader != "value" {
  63  		t.Fatalf("header: %q", gotHeader)
  64  	}
  65  	if gotRemote == "" {
  66  		t.Fatal("missing remote address")
  67  	}
  68  }
  69  
  70  func TestPostBodyAndDuplicateHeaders(t *testing.T) {
  71  	var body, multi string
  72  	_, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
  73  		body = string(req.Body)
  74  		multi = req.Header["x-multi"]
  75  		w.WriteHeader(201)
  76  		w.WriteString("created")
  77  	})
  78  	req, _ := http.NewRequest("POST", "http://"+addr+"/upload", strings.NewReader("payload"))
  79  	req.Header.Add("X-Multi", "one")
  80  	req.Header.Add("X-Multi", "two")
  81  	resp, err := http.DefaultClient.Do(req)
  82  	if err != nil {
  83  		t.Fatal(err)
  84  	}
  85  	resp.Body.Close()
  86  	if resp.StatusCode != 201 {
  87  		t.Fatalf("status %d", resp.StatusCode)
  88  	}
  89  	if body != "payload" {
  90  		t.Fatalf("body %q", body)
  91  	}
  92  	if multi != "one, two" {
  93  		t.Fatalf("combined headers %q", multi)
  94  	}
  95  }
  96  
  97  func TestExpectContinue(t *testing.T) {
  98  	var body string
  99  	_, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
 100  		body = string(req.Body)
 101  		w.WriteHeader(200)
 102  		w.WriteString("done")
 103  	})
 104  	conn, err := net.Dial("tcp", addr)
 105  	if err != nil {
 106  		t.Fatal(err)
 107  	}
 108  	defer conn.Close()
 109  	fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n", addr)
 110  	br := bufio.NewReader(conn)
 111  	line, _ := br.ReadString('\n')
 112  	if !strings.Contains(line, "100") {
 113  		t.Fatalf("expected 100 Continue, got %q", line)
 114  	}
 115  	// Consume the rest of the interim response.
 116  	for {
 117  		h, _ := br.ReadString('\n')
 118  		if h == "\r\n" || h == "" {
 119  			break
 120  		}
 121  	}
 122  	conn.Write([]byte("hello"))
 123  	final, _ := br.ReadString('\n')
 124  	if !strings.Contains(final, "200") {
 125  		t.Fatalf("final status %q", final)
 126  	}
 127  	if body != "hello" {
 128  		t.Fatalf("body %q", body)
 129  	}
 130  }
 131  
 132  func TestMalformedRequest(t *testing.T) {
 133  	_, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
 134  		t.Error("handler must not run")
 135  	})
 136  	conn, err := net.Dial("tcp", addr)
 137  	if err != nil {
 138  		t.Fatal(err)
 139  	}
 140  	defer conn.Close()
 141  	fmt.Fprintf(conn, "BROKEN\r\n\r\n")
 142  	br := bufio.NewReader(conn)
 143  	line, _ := br.ReadString('\n')
 144  	if !strings.Contains(line, "400") {
 145  		t.Fatalf("expected 400, got %q", line)
 146  	}
 147  }
 148  
 149  func TestOversizedBodyRejected(t *testing.T) {
 150  	srv := &Server{MaxBodyBytes: 4}
 151  	for i := 0; i < 40; i++ {
 152  		srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000))
 153  		if err := srv.Start(); err == nil {
 154  			break
 155  		}
 156  	}
 157  	if srv.ln == nil {
 158  		t.Fatal("could not bind")
 159  	}
 160  	go srv.Serve()
 161  	defer srv.Close()
 162  	time.Sleep(50 * time.Millisecond)
 163  	conn, err := net.Dial("tcp", srv.Addr)
 164  	if err != nil {
 165  		t.Fatal(err)
 166  	}
 167  	defer conn.Close()
 168  	fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 10\r\n\r\n0123456789", srv.Addr)
 169  	br := bufio.NewReader(conn)
 170  	line, _ := br.ReadString('\n')
 171  	if !strings.Contains(line, "400") {
 172  		t.Fatalf("expected 400, got %q", line)
 173  	}
 174  }
 175  
 176  func TestHijack(t *testing.T) {
 177  	_, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
 178  		conn, _, err := req.Hijack()
 179  		if err != nil {
 180  			return
 181  		}
 182  		conn.Write([]byte("RAW"))
 183  		conn.Close()
 184  	})
 185  	conn, err := net.Dial("tcp", addr)
 186  	if err != nil {
 187  		t.Fatal(err)
 188  	}
 189  	defer conn.Close()
 190  	fmt.Fprintf(conn, "GET /ws HTTP/1.1\r\nHost: %s\r\n\r\n", addr)
 191  	conn.SetReadDeadline(time.Now().Add(3 * time.Second))
 192  	got, _ := io.ReadAll(conn)
 193  	if string(got) != "RAW" {
 194  		t.Fatalf("hijacked bytes: %q", got)
 195  	}
 196  }
 197  
 198  func TestPathParamAndHelpers(t *testing.T) {
 199  	req := &Request{Path: "/list/abcdef"}
 200  	if req.PathParam() != "abcdef" {
 201  		t.Fatalf("PathParam %q", req.PathParam())
 202  	}
 203  	if StatusText(404) != "Not Found" || StatusText(299) != "Status" {
 204  		t.Fatal("status text")
 205  	}
 206  	if canonicalHeader("content-length") != "Content-Length" {
 207  		t.Fatalf("canonical header %q", canonicalHeader("content-length"))
 208  	}
 209  }
 210