From 679a62a9407c09be0cfb4e22455dca5ae694ce01 Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Mon, 16 Sep 2019 15:56:31 -0400 Subject: Flesh out --- caesar.go | 15 +++++------ caesar_test.go | 21 ++++++++++++++-- client/client.go | 43 +++++++++++++++++++++++++++++++ client/client_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ example_test.go | 22 ++++++++++++++++ fuzz_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 +++++- go.sum | 13 ++++++++++ server/auth.go | 15 +++++++++++ server/main.go | 34 +++++++++++++++++++++++++ server/main_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++ server/server | Bin 0 -> 7349148 bytes testdata/tests.txt | 3 +++ unusual_test.go | 11 ++++++++ 14 files changed, 365 insertions(+), 12 deletions(-) create mode 100644 client/client.go create mode 100644 client/client_test.go create mode 100644 example_test.go create mode 100644 fuzz_test.go create mode 100644 go.sum create mode 100644 server/auth.go create mode 100644 server/main.go create mode 100644 server/main_test.go create mode 100755 server/server create mode 100644 testdata/tests.txt create mode 100644 unusual_test.go diff --git a/caesar.go b/caesar.go index d62a26e..191c5b8 100644 --- a/caesar.go +++ b/caesar.go @@ -21,16 +21,13 @@ type RuneRange struct { End rune } -// Contains checks whether r is in the rune range. -func (rr RuneRange) Contains(r rune) bool { +// contains checks whether r is in the rune range. +func (rr RuneRange) contains(r rune) bool { return r >= rr.Start && r <= rr.End } -// Shift shifts r by d within the range, modulo the size of the range. -func (rr RuneRange) Shift(r rune, d int) rune { - if !rr.Contains(r) { - return r - } +// shift shifts r by d within the range, modulo the size of the range. +func (rr RuneRange) shift(r rune, d int) rune { return rr.Start + (r - rr.Start + rune(d)%(rr.End-rr.Start)) } @@ -62,8 +59,8 @@ type shiftFunc func(rune) rune func (c Coder) shifter(delta int) shiftFunc { return func(r rune) rune { for _, rr := range c.Ranges { - if rr.Contains(r) { - return rr.Shift(r, delta) + if rr.contains(r) { + return rr.shift(r, delta) } } return r diff --git a/caesar_test.go b/caesar_test.go index e7dc3c0..04b853b 100644 --- a/caesar_test.go +++ b/caesar_test.go @@ -4,14 +4,31 @@ import ( "testing" ) +func TestEncode(t *testing.T) { + msg := "Attack at dawn" + if Encode(msg) != "Dwwdfn dw gdzq" { + t.Fail() + } +} + func TestCaesar(t *testing.T) { + if testing.Short() { + t.Skip() + } msg := "Attack at dawn" + t.Logf("testing encoding message %s", msg) encoded := Encode(msg) if encoded == msg { - t.Errorf("expected ciphertext and plaintext to differ") + t.Log("expected ciphertext and plaintext to differ") + t.Fail() } decoded := Decode(encoded) if decoded != msg { - t.Errorf("expected recovered plaintext to match message, but got: %s", decoded) + t.Logf("expected recovered plaintext to match message, but got: %s", decoded) + t.Fail() } } + +func BenchmarkCaesar(b *testing.B) { + b.Fail() +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..e39a6a1 --- /dev/null +++ b/client/client.go @@ -0,0 +1,43 @@ +package client + +import ( + "bytes" + "fmt" + "io" + "net/http" + "sync" + "time" +) + +type CaesarClient struct { + Endpoint string + Token string + + c *http.Client + once sync.Once +} + +func (c *CaesarClient) EncodeMessage(r io.Reader) (io.Reader, error) { + c.once.Do(func() { + c.c = &http.Client{Timeout: 10 * time.Second} + }) + req, err := http.NewRequest("POST", c.Endpoint, r) + if err != nil { + return nil, err + } + req.Header.Set("authorization", "token "+c.Token) + req.Header.Set("user-agent", "caesar-client/1.0") + resp, err := c.c.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http status %s", resp.Status) + } + var body bytes.Buffer + defer resp.Body.Close() + if _, err := io.Copy(&body, resp.Body); err != nil { + return nil, err + } + return &body, nil +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..fb1701c --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,66 @@ +package client + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestBadAuth(t *testing.T) { + var hf http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("authorization") != "token TEST-TOKEN" { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Write([]byte("encoded yay")) + } + tests := []struct { + name string + tok string + expectErr bool + expectBody bool + }{ + {"valid token", "TEST-TOKEN", false, true}, + {"empty token", "", true, false}, + {"invalid token", "bad token", true, false}, + } + server := httptest.NewServer(hf) + defer server.Close() + c := CaesarClient{Endpoint: server.URL} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := c.EncodeMessage(strings.NewReader("secret message")) + if err == nil { + t.Errorf("should have gotten error") + } + if r != nil { + t.Errorf("should not have gotten a reader") + } + }) + } +} + +func TestEncode(t *testing.T) { + var hf http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("response")) + } + server := httptest.NewServer(hf) + defer server.Close() + c := CaesarClient{Endpoint: server.URL} + r, err := c.EncodeMessage(strings.NewReader("secret message")) + if err != nil { + t.Errorf("should not have gotten error: %v", err) + } + if r == nil { + t.Errorf("should have gotten a reader") + } + resp, err := ioutil.ReadAll(r) + if err != nil { + t.Fatalf("could not read message: %v", err) + } + if string(resp) != "response" { + t.Errorf("expected 'response' but got '%s'", resp) + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..2ccf83b --- /dev/null +++ b/example_test.go @@ -0,0 +1,22 @@ +package caesar + +import ( + "fmt" +) + +func ExampleEncode() { + secret := Encode("attack at dawn") + fmt.Println(secret) + // Output: dwwdfn dw gdzq +} + +func ExampleCoder() { + c := Coder{ + Key: 5, + Ranges: []RuneRange{ + {Start: 'A', End: 'Z'}, + }, + } + fmt.Println(c.Encode("aAbBcCdD")) + // Output: aFbGcHdI +} diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..71fdcb1 --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,68 @@ +package caesar + +import ( + "bytes" + "flag" + "io" + "io/ioutil" + "os" + "testing" +) + +var ( + runFuzz bool + fuzzInput string +) + +func TestMain(m *testing.M) { + flag.BoolVar(&runFuzz, "fuzz", false, "run the fuzz tests") + flag.StringVar(&fuzzInput, "fuzz.input", "", "input file for fuzz test") + flag.Parse() + os.Exit(m.Run()) +} + +func TestFuzz(t *testing.T) { + if !runFuzz { + t.Skip("skipping fuzz test") + } + f, err := os.Open(fuzzInput) + if err != nil { + t.Fatalf("could not open fuzz input: %v", err) + } + defer f.Close() + data, err := ioutil.ReadAll(f) + if err != nil { + t.Fatalf("could not read fuzz input: %v", err) + } + buf := bytes.NewBuffer(data) + for { + // read input + inp, err := buf.ReadBytes('\t') + if err == io.EOF { + return + } else if err != nil { + t.Fatalf("read error: %v", err) + } + + // read expected output + exp, err := buf.ReadBytes('\n') + if err == io.EOF { + t.Fatalf("found input with no matching output") + } else if err != nil { + t.Fatalf("read error: %v", err) + } + + if len(inp) < 2 || len(exp) < 2 { + t.Fatalf("malformed input") + } + + result := Encode(string(inp[:len(inp)-2])) + if result != string(exp[:len(exp)-2]) { + t.Logf("input: %s", inp) + t.Logf("output: %s", result) + t.Logf("expected: %s", exp) + t.Fail() + } + } + Encode(string(data)) +} diff --git a/go.mod b/go.mod index 89c5530..5c4dc27 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ -module go.burwell.io/caesar +module bnbl.io/caesar go 1.13 + +require ( + github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70 // indirect + github.com/ogier/pflag v0.0.1 // indirect + golang.org/x/tools v0.0.0-20190916173350-3512ebf57407 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a9ced96 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70 h1:aUiPu6/iCjcsnNe/WkhsnMOq7vPmkYo9kFaMX5FiNZU= +github.com/emersion/go-maildir v0.0.0-20190727102040-941194b0ac70/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84= +github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= +github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190916173350-3512ebf57407 h1:8t/stSbOaNHdu6dn/zC9OeTTqND69t0sA6uEkZVcILg= +golang.org/x/tools v0.0.0-20190916173350-3512ebf57407/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..15ea164 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,15 @@ +package main + +import ( + "net/http" +) + +// An Authenticator takes an HTTP request and returns true iff it is allowed to +// use our service. +type Authenticator func(req *http.Request) bool + +func TokenAuthenticator() Authenticator { + return func(req *http.Request) bool { + return req.Header.Get("authorization") == "magic" + } +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..23f2959 --- /dev/null +++ b/server/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "io/ioutil" + "net/http" + + "bnbl.io/caesar" +) + +func main() { + server := handleCaesar(TokenAuthenticator()) + http.ListenAndServe(":8088", server) +} + +func handleCaesar(auth Authenticator) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !auth(r) { + w.WriteHeader(http.StatusUnauthorized) + return + } + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusBadRequest) + return + } + body, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + encoded := caesar.Encode(string(body)) + w.Write([]byte(encoded)) + } +} diff --git a/server/main_test.go b/server/main_test.go new file mode 100644 index 0000000..75ddcdb --- /dev/null +++ b/server/main_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHandleCaesarAllowed(t *testing.T) { + allowAll := func(r *http.Request) bool { return true } + bodyReader := strings.NewReader("hello world") + req := httptest.NewRequest("POST", "/", bodyReader) + resp := httptest.NewRecorder() + + handler := handleCaesar(allowAll) + handler(resp, req) + + if resp.Code != http.StatusOK { + t.Errorf("expected OK status %d but got %d", http.StatusOK, resp.Code) + } + + body := string(resp.Body.Bytes()) + if body != "khoor zruog" { + t.Errorf("got unexpected body %s", body) + } +} + +func TestCaesarNotAllowed(t *testing.T) { + denyAll := func(r *http.Request) bool { return false } + req := httptest.NewRequest("POST", "/", nil) + resp := httptest.NewRecorder() + + handler := handleCaesar(denyAll) + handler(resp, req) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("should not have been authorized") + } + + if len(resp.Body.Bytes()) != 0 { + t.Errorf("should not have gotten a response body") + } +} + +func TestCaesarBadRequest(t *testing.T) { + allowAll := func(r *http.Request) bool { return true } + req := httptest.NewRequest("GET", "/", nil) + resp := httptest.NewRecorder() + handler := handleCaesar(allowAll) + handler(resp, req) + if resp.Code != http.StatusBadRequest { + t.Errorf("status should have been bad request, got %d", resp.Code) + } + if len(resp.Body.Bytes()) != 0 { + t.Errorf("should not have gotten a response body") + } +} diff --git a/server/server b/server/server new file mode 100755 index 0000000..979db70 Binary files /dev/null and b/server/server differ diff --git a/testdata/tests.txt b/testdata/tests.txt new file mode 100644 index 0000000..49b590e --- /dev/null +++ b/testdata/tests.txt @@ -0,0 +1,3 @@ +inp lqs +a d +Z C diff --git a/unusual_test.go b/unusual_test.go new file mode 100644 index 0000000..8faa0af --- /dev/null +++ b/unusual_test.go @@ -0,0 +1,11 @@ +// +build unusual + +package caesar + +import ( + "testing" +) + +func TestUnusual(t *testing.T) { + t.Fatalf("Here is a test I never really want to run") +} -- cgit v1.2.3