Codebase list pollen / lintian-fixes/main pollen_test.go
lintian-fixes/main

Tree @lintian-fixes/main (Download .tar.gz)

pollen_test.go @lintian-fixes/mainraw · history · blame

package main

import (
	"bufio"
	"bytes"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"testing"
)

type logEntry struct {
	severity string
	message  string
}

type localLogger struct {
	logs []logEntry
}

func (l *localLogger) Close() error {
	l.logs = append(l.logs, logEntry{"close", ""})
	return nil
}

func (l *localLogger) Info(msg string) error {
	l.logs = append(l.logs, logEntry{"info", msg})
	return nil
}

func (l *localLogger) Err(msg string) error {
	l.logs = append(l.logs, logEntry{"err", msg})
	return nil
}

func (l *localLogger) Crit(msg string) error {
	l.logs = append(l.logs, logEntry{"crit", msg})
	return nil
}

func (l *localLogger) Emerg(msg string) error {
	l.logs = append(l.logs, logEntry{"emerg", msg})
	return nil
}

type Suite struct {
	*httptest.Server
	t      *testing.T
	dev    io.ReadWriter
	logger *localLogger
	pollen *PollenServer
}

func NewSuite(t *testing.T) *Suite {
	/* hardcode /dev/urandom for testing purposes */
	dev, err := os.OpenFile("/dev/urandom", os.O_RDWR, 0)
	if err != nil {
		t.Fatalf("Cannot open device: %s\n", err)
	}
	return NewSuiteWithDev(t, dev)
}

func NewSuiteWithDev(t *testing.T, dev io.ReadWriter) *Suite {
	logger := &localLogger{}
	handler := &PollenServer{randomSource: dev, log: logger, readSize: 64}
	return &Suite{httptest.NewServer(handler), t, dev, logger, handler}
}

func (s *Suite) Assert(v bool, args ...interface{}) {
	if !v {
		s.t.Error(args...)
	}
}

func (s *Suite) TearDown() {
	s.Server.Close()
	if closer, ok := s.dev.(io.Closer); ok {
		closer.Close()
	}
}

// MustScan scans a single token. There must be a token available and it must
// scan successfully or an error is returned.
func MustScan(s *bufio.Scanner) error {
	if !s.Scan() {
		return fmt.Errorf("Missing expected text")
	}
	return s.Err()
}

// ParseResp parses the pollen response to the challenge & response
// in the output, as well as any error that occurred with reading or
// validating it.
func ReadResp(r io.Reader) (challenge, response string, err error) {
	scanner := bufio.NewScanner(r)
	if err = MustScan(scanner); err != nil {
		return
	}
	challenge = scanner.Text()
	if err = MustScan(scanner); err != nil {
		return
	}
	response = scanner.Text()
	return
}

// CheckHex returns an error if the given string is not valid hex.
func CheckHex(s string) error {
	_, err := hex.DecodeString(s)
	return err
}

// TestNoChallenge tests the pollen service when no challenge is given
// in the request.
func TestNoChallenge(t *testing.T) {
	s := NewSuite(t)
	defer s.TearDown()

	res, err := http.Get(s.URL)
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	chal, seed, err := ReadResp(res.Body)
	s.Assert(err != nil, "response error:", err)
	s.Assert(res.StatusCode == http.StatusBadRequest, "didn't get Bad Request, got: ", res.Status)
	s.Assert(chal == usePollinateError, "got the wrong error message:", chal)
	s.Assert(seed == "", "got extra messages:", seed)
}

func (s *Suite) SanityCheck(chal, seed string) {
	s.Assert(chal != seed, "challenge response and seed were the same!")
	s.Assert(len(chal) == len(seed), "challenge response and seed length not equal")
	s.Assert(CheckHex(chal) == nil, "invalid hex:", chal)
	s.Assert(CheckHex(seed) == nil, "invalid hex:", seed)
}

// PorkChopSha512 is $(echo -n "pork chop sandwiches" | sha512sum)
const PorkChopSha512 = "a75751ccd71ba00d7b6c3b74cc0c02373f3f26c14dfe47afd580b0d87bf9fd8cebc73ea29b1cae15586e0d118922342ea7e94d0cb73a0f918d7d8c7ec065e873"

// TestPorkChopSandwiches tests the pollen service when given
// pork chop sandwiches.
func TestPorkChopSandwiches(t *testing.T) {
	s := NewSuite(t)
	defer s.TearDown()

	res, err := http.Get(s.URL + "?challenge=pork+chop+sandwiches")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	chal, resp, err := ReadResp(res.Body)
	s.Assert(err == nil, "response error:", err)
	s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
	s.SanityCheck(chal, resp)
}

// TestPorkChopPost tests the pollen service when the
// pork chop sandwiches are POSTed.
func TestPostChopSandwiches(t *testing.T) {
	s := NewSuite(t)
	defer s.TearDown()

	res, err := http.PostForm(s.URL, url.Values{"challenge": []string{"pork chop sandwiches"}})
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	chal, resp, err := ReadResp(res.Body)
	s.Assert(err == nil, "response error:", err)
	s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
	s.SanityCheck(chal, resp)
}

const UniqueChainRounds = 100

// TestUniqueChaining tests the uniqueness of seeds and challenge responses
// when fed into successive requests as challenges.
func TestUniqueChaining(t *testing.T) {
	s := NewSuite(t)
	defer s.TearDown()

	challengeResps := make(map[string]bool)
	seeds := make(map[string]bool)
	challenge := "the bassomatic '76"
	for i := 0; i < UniqueChainRounds; i++ {
		res, err := http.Get(fmt.Sprintf("%s/?challenge=%s", s.URL, url.QueryEscape(challenge)))
		s.Assert(err == nil, "http client error:", err)

		challengeResp, seed, err := ReadResp(res.Body)
		err = res.Body.Close()
		s.Assert(err == nil, "response error:", err)

		challengeResps[challengeResp] = true
		seeds[seed] = true

		challenge = seed
	}
	s.Assert(len(challengeResps) == UniqueChainRounds, "non-unique challenge response")
	s.Assert(len(seeds) == UniqueChainRounds, "non-unique seed response")
}

// TestUniqueSeeds tests the uniqueness of responses to the same challenge
func TestUniqueSeeds(t *testing.T) {
	s := NewSuite(t)
	defer s.TearDown()

	challengeResps := make(map[string]bool)
	seeds := make(map[string]bool)
	challenge := "the bassomatic '76"
	for i := 0; i < UniqueChainRounds; i++ {
		res, err := http.Get(fmt.Sprintf("%s/?challenge=%s", s.URL, url.QueryEscape(challenge)))
		s.Assert(err == nil, "http client error:", err)

		challengeResp, seed, err := ReadResp(res.Body)
		err = res.Body.Close()
		s.Assert(err == nil, "response error:", err)

		challengeResps[challengeResp] = true
		seeds[seed] = true
	}
	s.Assert(len(challengeResps) == 1, "more than one sha sum for the same challenge")
	s.Assert(len(seeds) == UniqueChainRounds, "non-unique seed response")
}

// DilbertRandom is 64 bytes of pure nines
var DilbertRandom = "ninenineninenineninenineninenineninenineninenineninenineninenine"
var DilbertRandomSHA1 = "f73655d899f0f3d181d8e94b163e774a05abdd3b55123d0b9b2f18ad8c05c76e6fde93ba9dfc350acc2e378b59dd6962fc305b741f9a5b7edb16435e61a86b96"

// TestCannedContent exercises the input and output removing the randomness of rand
func TestCannedContent(t *testing.T) {
	b := bytes.NewBufferString(DilbertRandom)
	s := NewSuiteWithDev(t, b)
	defer s.TearDown()

	res, err := http.Get(s.URL + "?challenge=pork+chop+sandwiches")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	chal, seed, err := ReadResp(res.Body)
	s.Assert(err == nil, "response error:", err)
	s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
	s.SanityCheck(chal, seed)
	// Check that the 'random' seed we got back was appropriately mixed
	// with the challenge
	s.Assert(seed != DilbertRandom, "got the raw random content")
	s.Assert(seed != DilbertRandomSHA1, "got the sha of random content without the challenge")
	expectedSum := sha512.New()
	io.WriteString(expectedSum, "pork chop sandwiches")
	io.WriteString(expectedSum, DilbertRandom)
	expectedSeed := fmt.Sprintf("%x", expectedSum.Sum(nil))
	s.Assert(seed == expectedSeed, "expected:", expectedSeed, "got:", seed)
	// We can also check that the challenge was correctly written to our random device
	// b.Bytes() is the remainder of our buffer, and Buffer writes at the end
	// This also shows that we didn't write the raw request
	writtenBytesInHex := fmt.Sprintf("%x", string(b.Bytes()))
	s.Assert(PorkChopSha512 == writtenBytesInHex, "expected:", PorkChopSha512, "got:", writtenBytesInHex)
}

// TestSizeMatters asserts that changing 'size' changes how many bytes we read
func TestSizeMatters(t *testing.T) {
	b := bytes.NewBufferString(DilbertRandom)
	s := NewSuiteWithDev(t, b)
	defer s.TearDown()

	s.pollen.readSize = 32
	res, err := http.Get(s.URL + "?challenge=xxx")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	_, _, err = ReadResp(res.Body)
	s.Assert(err == nil, "response err:", err)
	// If we set the readSize to 32 bytes, then we should only have that
	// much data read from the buffer
	remaining := b.Bytes()
	// We have to add the 64 bytes that we wrote because of the challenge
	s.Assert(len(remaining) == 32+64, "wrong number of bytes remaining, expected 96 got:", len(remaining))
}

// TestExtraSize asserts that you can make size 'big'
func TestExtraSize(t *testing.T) {
	b := bytes.NewBufferString(DilbertRandom)
	s := NewSuiteWithDev(t, b)
	defer s.TearDown()

	// We only start with 64 bytes of "nine" but we add the challenge to the pool
	s.pollen.readSize = 128
	res, err := http.Get(s.URL + "?challenge=xxx")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	_, _, err = ReadResp(res.Body)
	s.Assert(err == nil, "response err:", err)
	remaining := b.Bytes()
	s.Assert(len(remaining) == 0, "wrong number of bytes remaining, expected 0 got:", len(remaining))
}

type OnlyReader struct {
	*bytes.Buffer
}

func (o *OnlyReader) Write([]byte) (int, error) {
	return 0, &os.PathError{Op: "write", Path: "<mem>", Err: os.ErrPermission}
}

// We have to implement this because bytes.Buffer does, and io.WriteString can chose to use it
func (o *OnlyReader) WriteString(string) (int, error) {
	return 0, &os.PathError{Op: "write", Path: "<mem>", Err: os.ErrPermission}
}

// TestWriteFailure tests that if we can't write to our random device, we keep going
func TestWriteFailure(t *testing.T) {
	b := &OnlyReader{bytes.NewBufferString(DilbertRandom)}
	s := NewSuiteWithDev(t, b)
	defer s.TearDown()

	res, err := http.Get(s.URL + "?challenge=xxx")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	chal, seed, err := ReadResp(res.Body)
	s.Assert(err == nil, "response err:", err)
	s.SanityCheck(chal, seed)
	// Failing to write to the random device is logged
	s.Assert(len(s.logger.logs) == 3, "expected 3 log messages, got:", len(s.logger.logs))
	start := "Cannot write to random device at ["
	s.Assert(s.logger.logs[0].severity == "err" &&
		s.logger.logs[0].message[:len(start)] == start,
		"didn't get the expected error message, got:", s.logger.logs[0])
	start = "Server received challenge from ["
	s.Assert(s.logger.logs[1].severity == "info" &&
		s.logger.logs[1].message[:len(start)] == start,
		"didn't get the expected error message, got:", s.logger.logs[1])
	start = "Server sent response to ["
	s.Assert(s.logger.logs[2].severity == "info" &&
		s.logger.logs[2].message[:len(start)] == start,
		"didn't get the expected error message, got:", s.logger.logs[2])
}

type FailingReader struct {
	*bytes.Buffer
}

func (o *FailingReader) Read([]byte) (int, error) {
	return 0, &os.PathError{Op: "read", Path: "<mem>", Err: os.ErrPermission}
}

// TestReadFailure tests that if we can't read from our random device it is immediately fatal
func TestReadFailure(t *testing.T) {
	// No random data to give to the client
	b := &FailingReader{bytes.NewBufferString("")}
	s := NewSuiteWithDev(t, b)
	defer s.TearDown()

	res, err := http.Get(s.URL + "?challenge=xxx")
	s.Assert(err == nil, "http client error:", err)
	defer res.Body.Close()
	errMsg, _, err := ReadResp(res.Body)
	s.Assert(err != nil, "response error:", err)
	s.Assert(errMsg == "Failed to read from random device", "wrong error: ", errMsg)
	s.Assert(res.StatusCode == http.StatusInternalServerError, "wrong status: ", res.Status)
	s.Assert(len(s.logger.logs) == 2, "expected 2 log messages, got: ", len(s.logger.logs))
	start := "Server received challenge from ["
	s.Assert(s.logger.logs[0].severity == "info" &&
		s.logger.logs[0].message[:len(start)] == start,
		"didn't get the expected error message, got:", s.logger.logs[0])
	start = "Cannot read from random device at ["
	s.Assert(s.logger.logs[1].severity == "err" &&
		s.logger.logs[1].message[:len(start)] == start,
		"didn't get the expected error message, got:", s.logger.logs[1])
}