Add HW 3
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
package hw3test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Contract cases use a small separate tree, so snapshots never scan G6/G7 files.
|
||||
func RunContractTests(t *TC, runner Runner, tmpRoot, group string) {
|
||||
t.RunByName("contract", func(t *TC) {
|
||||
workdir, err := os.MkdirTemp(tmpRoot, "contract-")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(workdir)
|
||||
require.NoError(t, os.Chmod(workdir, 0755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(workdir, "dir", "nested"), 0755))
|
||||
ascii := []byte("hello\r\nworld\n")
|
||||
binary := []byte{0, 255, 13, 10, 13, 10, 128, 1}
|
||||
if group == "G1" || group == "G3" {
|
||||
binary = ascii
|
||||
}
|
||||
for name, body := range map[string][]byte{
|
||||
"alpha.txt": ascii, "binary": binary, "empty": {},
|
||||
".hidden": []byte("hidden"), "dir/nested/child": []byte("child"),
|
||||
} {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workdir, filepath.FromSlash(name)), body, 0644))
|
||||
}
|
||||
port, err := GetFreePort()
|
||||
require.NoError(t, err)
|
||||
opts := RunOpts{Port: port, WorkingDirectory: workdir, ListenAddr: "0.0.0.0", ServerDomain: "localhost"}
|
||||
opts.GenerateRunConfig(t, rand.New(rand.NewSource(9103)), &EnvGen{AllowEnv: true})
|
||||
stop, err := runner.Run(t, opts)
|
||||
require.NoError(t, err)
|
||||
defer stop()
|
||||
require.NoError(t, WaitForServer(t, opts))
|
||||
extra := group == "G5" || group == "G7"
|
||||
|
||||
request := func(method, path, headers string, body []byte) string {
|
||||
return fmt.Sprintf("%s %s HTTP/1.1\r\nhOsT: LOCALHOST\r\ncOnTeNt-LeNgTh: %d\r\n%s\r\n%s", method, path, len(body), headers, body)
|
||||
}
|
||||
run := func(name, raw string, split bool, codes []int, expected []byte, unchanged bool) {
|
||||
t.RunByName(name, func(t *TC) {
|
||||
before, err := snapshotTree(workdir)
|
||||
require.NoError(t, err)
|
||||
parts := [][]byte{[]byte(raw)}
|
||||
if split {
|
||||
// Split inside a header and inside CRLFCRLF; coalesce its end with body bytes.
|
||||
boundary := strings.Index(raw, "\r\n\r\n")
|
||||
parts = [][]byte{[]byte(raw[:9]), []byte(raw[9 : boundary+3]), []byte(raw[boundary+3:])}
|
||||
}
|
||||
resp, body := contractExchange(t, opts, parts)
|
||||
require.Contains(t, codes, resp.StatusCode)
|
||||
if resp.StatusCode >= 400 {
|
||||
require.NotEmpty(t, bytes.TrimSpace(body), "error explanation is empty")
|
||||
}
|
||||
if extra {
|
||||
require.NotEmpty(t, resp.Header.Get("Server"))
|
||||
if len(body) > 0 {
|
||||
mediaType, err := parseContentType(resp.Header.Get("Content-Type"))
|
||||
require.NoError(t, err)
|
||||
if methodIsDirectoryGet(raw) && resp.StatusCode == 200 {
|
||||
require.Contains(t, []string{"text/plain", "text/html"}, mediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
if expected != nil {
|
||||
require.Empty(t, resp.Header.Get("Content-Encoding"))
|
||||
require.Equal(t, expected, body)
|
||||
}
|
||||
if unchanged {
|
||||
after, err := snapshotTree(workdir)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, compareSnapshots(before, after))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if group == "G4" || group == "G5" || group == "G6" || group == "G7" {
|
||||
run("post-root-file", request("POST", "/", "", binary), false, []int{409}, nil, true)
|
||||
run("post-root-dir", request("POST", "/", "Create-Directory: True\r\n", nil), false, []int{409}, nil, true)
|
||||
run("put-root", request("PUT", "/", "", binary), false, []int{409}, nil, true)
|
||||
for _, value := range []string{"absent", "False", "True"} {
|
||||
header := ""
|
||||
if value != "absent" {
|
||||
header = "Remove-Directory: " + value + "\r\n"
|
||||
}
|
||||
run("delete-root-"+value, request("DELETE", "/", header, nil), false, []int{403}, nil, true)
|
||||
}
|
||||
if extra {
|
||||
raw := strings.Replace(request("DELETE", "/", "Remove-Directory: True\r\n", nil), "LOCALHOST", "wrong.example", 1)
|
||||
run("wrong-host-delete-root", raw, false, []int{400}, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
switch group {
|
||||
case "G1":
|
||||
run("split-headers", request("GET", "/alpha.txt", "", nil), true, []int{200}, ascii, true)
|
||||
// No Content-Length is also valid for an empty request.
|
||||
run("no-request-body", "GET /alpha.txt HTTP/1.1\r\nHost: localhost\r\n\r\n", false, []int{200}, ascii, true)
|
||||
run("empty-file", request("GET", "/empty", "", nil), false, []int{200}, []byte{}, true)
|
||||
case "G2":
|
||||
run("binary-body", request("GET", "/binary", "", nil), true, []int{200}, binary, true)
|
||||
case "G3":
|
||||
run("missing", request("GET", "/missing", "", nil), false, []int{404}, nil, true)
|
||||
t.RunByName("root-listing", func(t *TC) {
|
||||
resp, body := contractExchange(t, opts, [][]byte{[]byte(request("GET", "/", "", nil))})
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
require.Empty(t, resp.Header.Get("Content-Encoding"))
|
||||
for _, name := range []string{"alpha.txt", "binary", "empty", ".hidden", "dir"} {
|
||||
require.Contains(t, string(body), name)
|
||||
}
|
||||
})
|
||||
case "G4", "G5":
|
||||
for _, tc := range []struct {
|
||||
name, method, path, headers string
|
||||
code int
|
||||
}{
|
||||
{"post-existing-file", "POST", "/alpha.txt", "", 409},
|
||||
{"post-existing-dir", "POST", "/dir", "Create-Directory: True\r\n", 409},
|
||||
{"post-missing-parent", "POST", "/missing/new", "", 404},
|
||||
{"post-dir-missing-parent", "POST", "/missing/new", "Create-Directory: True\r\n", 404},
|
||||
{"post-file-as-parent", "POST", "/alpha.txt/child", "", 404},
|
||||
{"put-missing", "PUT", "/missing", "", 404},
|
||||
{"put-dir", "PUT", "/dir", "", 409},
|
||||
{"delete-missing", "DELETE", "/missing", "", 404},
|
||||
{"delete-dir-absent", "DELETE", "/dir", "", 406},
|
||||
{"delete-dir-false", "DELETE", "/dir", "rEmOvE-dIrEcToRy: False\r\n", 406},
|
||||
} {
|
||||
body := []byte(nil)
|
||||
if tc.method == "PUT" || (tc.method == "POST" && tc.headers == "") {
|
||||
body = binary
|
||||
}
|
||||
run(tc.name, request(tc.method, tc.path, tc.headers, body), false, []int{tc.code}, nil, true)
|
||||
}
|
||||
run("create-file-false", request("POST", "/new", "cReAtE-dIrEcToRy: False\r\n", binary), true, []int{200, 201}, nil, false)
|
||||
run("read-created", request("GET", "/new", "", nil), false, []int{200}, binary, true)
|
||||
run("create-empty", request("POST", "/new-empty", "", nil), false, []int{200, 201}, nil, false)
|
||||
run("read-empty", request("GET", "/new-empty", "", nil), false, []int{200}, []byte{}, true)
|
||||
run("replace-shorter", request("PUT", "/alpha.txt", "", []byte("x")), true, []int{200, 204}, nil, false)
|
||||
run("read-shorter", request("GET", "/alpha.txt", "", nil), false, []int{200}, []byte("x"), true)
|
||||
run("replace-empty", request("PUT", "/alpha.txt", "", nil), false, []int{200, 204}, nil, false)
|
||||
run("read-replaced-empty", request("GET", "/alpha.txt", "", nil), false, []int{200}, []byte{}, true)
|
||||
run("create-dir", request("POST", "/created-dir", "cReAtE-dIrEcToRy: True\r\n", nil), false, []int{200, 201}, nil, false)
|
||||
t.RunByName("created-dir-on-disk", func(t *TC) {
|
||||
entries, err := os.ReadDir(filepath.Join(workdir, "created-dir"))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, entries)
|
||||
})
|
||||
run("recursive-delete", request("DELETE", "/dir", "rEmOvE-dIrEcToRy: True\r\n", nil), false, []int{200}, nil, false)
|
||||
t.RunByName("deleted-tree-on-disk", func(t *TC) {
|
||||
_, err := os.Stat(filepath.Join(workdir, "dir"))
|
||||
require.True(t, os.IsNotExist(err), "directory must be removed")
|
||||
})
|
||||
if extra {
|
||||
for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
|
||||
raw := strings.Replace(request(method, "/alpha.txt", "", []byte("changed")), "LOCALHOST", "wrong.example", 1)
|
||||
run("wrong-host-"+method, raw, false, []int{400}, nil, true)
|
||||
}
|
||||
}
|
||||
case "G7":
|
||||
for _, path := range []string{"/binary", "/dir", "/empty"} {
|
||||
t.RunByName("gzip-"+strings.TrimPrefix(path, "/"), func(t *TC) {
|
||||
before, err := snapshotTree(workdir)
|
||||
require.NoError(t, err)
|
||||
resp, body := contractExchange(t, opts, [][]byte{[]byte(request("GET", path, "aCcEpT-eNcOdInG: gzip\r\n", nil))})
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
require.Equal(t, "gzip", resp.Header.Get("Content-Encoding"))
|
||||
require.NotEmpty(t, resp.Header.Get("Server"))
|
||||
mediaType, err := parseContentType(resp.Header.Get("Content-Type"))
|
||||
require.NoError(t, err)
|
||||
gz, err := gzip.NewReader(bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
decoded, err := io.ReadAll(gz)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gz.Close())
|
||||
if path == "/binary" {
|
||||
require.Equal(t, binary, decoded)
|
||||
}
|
||||
if path == "/empty" {
|
||||
require.Empty(t, decoded)
|
||||
}
|
||||
if path == "/dir" {
|
||||
require.Contains(t, string(decoded), "nested")
|
||||
require.Contains(t, []string{"text/plain", "text/html"}, mediaType)
|
||||
}
|
||||
after, err := snapshotTree(workdir)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, compareSnapshots(before, after))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func methodIsDirectoryGet(raw string) bool { return strings.HasPrefix(raw, "GET / HTTP/") }
|
||||
|
||||
// The client leaves its sending side open: a server reading to EOF must time out.
|
||||
func contractExchange(t *TC, opts RunOpts, parts [][]byte) (*http.Response, []byte) {
|
||||
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(bytes.Join(parts, nil))))
|
||||
require.NoError(t, err)
|
||||
defer req.Body.Close()
|
||||
address, err := url.Parse(opts.Address())
|
||||
require.NoError(t, err)
|
||||
conn, err := net.DialTimeout("tcp", address.Host, 10*time.Second)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
require.NoError(t, conn.SetDeadline(time.Now().Add(10*time.Second)))
|
||||
for i, part := range parts {
|
||||
_, err := io.Copy(conn, bytes.NewReader(part))
|
||||
require.NoError(t, err)
|
||||
if i+1 < len(parts) {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
resp, body, err := readContractResponse(bufio.NewReader(conn), req.ContentLength > 0)
|
||||
require.NoError(t, err)
|
||||
return resp, body
|
||||
}
|
||||
|
||||
func readContractResponse(reader *bufio.Reader, requestHasBody bool) (*http.Response, []byte, error) {
|
||||
resp, err := http.ReadResponse(reader, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if err := validateResponseFraming(resp); err != nil {
|
||||
return resp, nil, err
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp, nil, err
|
||||
}
|
||||
if resp.StatusCode != 204 && int64(len(body)) != resp.ContentLength {
|
||||
return resp, nil, fmt.Errorf("response length mismatch")
|
||||
}
|
||||
if err := checkResponseEnd(reader, requestHasBody); err != nil {
|
||||
return resp, nil, err
|
||||
}
|
||||
return resp, body, nil
|
||||
}
|
||||
|
||||
type treeEntry struct {
|
||||
Mode os.FileMode
|
||||
Size int64
|
||||
Digest [32]byte
|
||||
}
|
||||
|
||||
func snapshotTree(root string) (map[string]treeEntry, error) {
|
||||
result := make(map[string]treeEntry)
|
||||
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(root, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry := treeEntry{Mode: info.Mode()}
|
||||
if info.Mode().IsRegular() {
|
||||
entry.Size = info.Size()
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := sha256.New()
|
||||
_, copyErr := io.Copy(h, f)
|
||||
closeErr := f.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
copy(entry.Digest[:], h.Sum(nil))
|
||||
}
|
||||
result[rel] = entry
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func compareSnapshots(before, after map[string]treeEntry) error {
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
return fmt.Errorf("request changed the file system unexpectedly")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user