Add test_upload_corrupt_blob and fix the Go server.

The Go server was forgetting the final step of verifying the hash it
computed.

The App Engine server currently fails the test because the failure
mode is specified in the spec.  TODO: clarify that.

This also adds some unit tests for camli/blobref, and changes BlobRef
to a struct, instead of an interface.
This commit is contained in:
Brad Fitzpatrick 2010-12-17 09:59:03 -08:00
parent 82278075fc
commit 2627a6cf4c
8 changed files with 120 additions and 40 deletions

View File

@ -17,12 +17,9 @@ var supportedDigests = map[string]func()hash.Hash{
}, },
} }
type BlobRef interface { type BlobRef struct {
HashName() string hashName string
Digest() string digest string
Hash() hash.Hash
IsSupported() bool
fmt.Stringer
} }
type ReadSeekCloser interface { type ReadSeekCloser interface {
@ -32,27 +29,22 @@ type ReadSeekCloser interface {
} }
type Fetcher interface { type Fetcher interface {
Fetch(BlobRef) (file ReadSeekCloser, size int64, err os.Error) Fetch(*BlobRef) (file ReadSeekCloser, size int64, err os.Error)
} }
type blobRef struct { func (b *BlobRef) HashName() string {
hashName string
digest string
}
func (b *blobRef) HashName() string {
return b.hashName return b.hashName
} }
func (b *blobRef) Digest() string { func (b *BlobRef) Digest() string {
return b.digest return b.digest
} }
func (o *blobRef) String() string { func (o *BlobRef) String() string {
return fmt.Sprintf("%s-%s", o.hashName, o.digest) return fmt.Sprintf("%s-%s", o.hashName, o.digest)
} }
func (o *blobRef) Hash() hash.Hash { func (o *BlobRef) Hash() hash.Hash {
fn, ok := supportedDigests[o.hashName] fn, ok := supportedDigests[o.hashName]
if !ok { if !ok {
return nil return nil
@ -60,7 +52,11 @@ func (o *blobRef) Hash() hash.Hash {
return fn() return fn()
} }
func (o *blobRef) IsSupported() bool { func (o *BlobRef) HashMatches(h hash.Hash) bool {
return fmt.Sprintf("%x", h.Sum()) == o.digest
}
func (o *BlobRef) IsSupported() bool {
_, ok := supportedDigests[o.hashName] _, ok := supportedDigests[o.hashName]
return ok return ok
} }
@ -70,17 +66,17 @@ var kExpectedDigestSize = map[string]int{
"sha1": 40, "sha1": 40,
} }
func blobIfValid(hashname, digest string) BlobRef { func blobIfValid(hashname, digest string) *BlobRef {
expectedSize := kExpectedDigestSize[hashname] expectedSize := kExpectedDigestSize[hashname]
if expectedSize != 0 && len(digest) != expectedSize { if expectedSize != 0 && len(digest) != expectedSize {
return nil return nil
} }
return &blobRef{hashname, digest} return &BlobRef{hashname, digest}
} }
// FromPattern takes a pattern and if it matches 's' with two exactly two valid // FromPattern takes a pattern and if it matches 's' with two exactly two valid
// submatches, returns a BlobRef, else returns nil. // submatches, returns a BlobRef, else returns nil.
func FromPattern(r *regexp.Regexp, s string) BlobRef { func FromPattern(r *regexp.Regexp, s string) *BlobRef {
matches := r.FindStringSubmatch(s) matches := r.FindStringSubmatch(s)
if len(matches) != 3 { if len(matches) != 3 {
return nil return nil
@ -88,6 +84,6 @@ func FromPattern(r *regexp.Regexp, s string) BlobRef {
return blobIfValid(matches[1], matches[2]) return blobIfValid(matches[1], matches[2])
} }
func Parse(ref string) BlobRef { func Parse(ref string) *BlobRef {
return FromPattern(kBlobRefPattern, ref) return FromPattern(kBlobRefPattern, ref)
} }

View File

@ -0,0 +1,41 @@
package blobref
import (
"testing"
)
func TestAll(t *testing.T) {
br := Parse("sha1-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")
if br == nil {
t.Fatalf("Failed to parse blobref")
}
if br.hashName != "sha1" {
t.Errorf("Expected sha1 hashName")
}
if br.digest != "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" {
t.Errorf("Invalid digest")
}
if !br.IsSupported() {
t.Errorf("sha1 should be supported")
}
hash := br.Hash()
hash.Write([]byte("foo"))
if !br.HashMatches(hash) {
t.Errorf("Expected hash of bytes 'foo' to match")
}
hash.Write([]byte("bogusextra"))
if br.HashMatches(hash) {
t.Errorf("Unexpected hash match with bogus extra bytes")
}
}
func TestNotSupported(t *testing.T) {
br := Parse("unknownfunc-0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")
if br == nil {
t.Fatalf("Failed to parse blobref")
}
if br.IsSupported() {
t.Fatalf("Unexpected IsSupported() on unknownfunc")
}
}

View File

@ -14,7 +14,7 @@ import (
const maxEnumerate = 100000 const maxEnumerate = 100000
type blobInfo struct { type blobInfo struct {
blobref.BlobRef *blobref.BlobRef
*os.FileInfo *os.FileInfo
os.Error os.Error
} }
@ -127,4 +127,3 @@ func handleEnumerateBlobs(conn http.ResponseWriter, req *http.Request) {
} }
fmt.Fprintf(conn, "\n}\n") fmt.Fprintf(conn, "\n}\n")
} }

View File

@ -11,7 +11,7 @@ type diskStorage struct {
Root string Root string
} }
func (ds *diskStorage) Fetch(blob blobref.BlobRef) (blobref.ReadSeekCloser, int64, os.Error) { func (ds *diskStorage) Fetch(blob *blobref.BlobRef) (blobref.ReadSeekCloser, int64, os.Error) {
fileName := BlobFileName(blob) fileName := BlobFileName(blob)
stat, err := os.Stat(fileName) stat, err := os.Stat(fileName)
if err == os.ENOENT { if err == os.ENOENT {
@ -30,20 +30,20 @@ func newDiskStorage(root string) *diskStorage {
var kGetPutPattern *regexp.Regexp = regexp.MustCompile(`^/camli/([a-z0-9]+)-([a-f0-9]+)$`) var kGetPutPattern *regexp.Regexp = regexp.MustCompile(`^/camli/([a-z0-9]+)-([a-f0-9]+)$`)
func BlobFileBaseName(b blobref.BlobRef) string { func BlobFileBaseName(b *blobref.BlobRef) string {
return fmt.Sprintf("%s-%s.dat", b.HashName(), b.Digest()) return fmt.Sprintf("%s-%s.dat", b.HashName(), b.Digest())
} }
func BlobDirectoryName(b blobref.BlobRef) string { func BlobDirectoryName(b *blobref.BlobRef) string {
d := b.Digest() d := b.Digest()
return fmt.Sprintf("%s/%s/%s/%s", *flagStorageRoot, b.HashName(), d[0:3], d[3:6]) return fmt.Sprintf("%s/%s/%s/%s", *flagStorageRoot, b.HashName(), d[0:3], d[3:6])
} }
func BlobFileName(b blobref.BlobRef) string { func BlobFileName(b *blobref.BlobRef) string {
return fmt.Sprintf("%s/%s-%s.dat", BlobDirectoryName(b), b.HashName(), b.Digest()) return fmt.Sprintf("%s/%s-%s.dat", BlobDirectoryName(b), b.HashName(), b.Digest())
} }
func BlobFromUrlPath(path string) blobref.BlobRef { func BlobFromUrlPath(path string) *blobref.BlobRef {
return blobref.FromPattern(kGetPutPattern, path) return blobref.FromPattern(kGetPutPattern, path)
} }

View File

@ -12,10 +12,12 @@ import (
) )
type receivedBlob struct { type receivedBlob struct {
blobRef blobref.BlobRef blobRef *blobref.BlobRef
size int64 size int64
} }
var CorruptBlobError = os.NewError("corrupt blob; digest doesn't match")
func handleMultiPartUpload(conn http.ResponseWriter, req *http.Request) { func handleMultiPartUpload(conn http.ResponseWriter, req *http.Request) {
if !(req.Method == "POST" && req.URL.Path == "/camli/upload") { if !(req.Method == "POST" && req.URL.Path == "/camli/upload") {
httputil.BadRequestError(conn, "Inconfigured handler.") httputil.BadRequestError(conn, "Inconfigured handler.")
@ -88,7 +90,7 @@ func commonUploadResponse(req *http.Request) map[string]interface{} {
return ret return ret
} }
func receiveBlob(blobRef blobref.BlobRef, source io.Reader) (blobGot *receivedBlob, err os.Error) { func receiveBlob(blobRef *blobref.BlobRef, source io.Reader) (blobGot *receivedBlob, err os.Error) {
hashedDirectory := BlobDirectoryName(blobRef) hashedDirectory := BlobDirectoryName(blobRef)
err = os.MkdirAll(hashedDirectory, 0700) err = os.MkdirAll(hashedDirectory, 0700)
if err != nil { if err != nil {
@ -115,10 +117,16 @@ func receiveBlob(blobRef blobref.BlobRef, source io.Reader) (blobGot *receivedBl
if err != nil { if err != nil {
return return
} }
// TODO: fsync before close.
if err = tempFile.Close(); err != nil { if err = tempFile.Close(); err != nil {
return return
} }
if !blobRef.HashMatches(hash) {
err = CorruptBlobError
return
}
fileName := BlobFileName(blobRef) fileName := BlobFileName(blobRef)
if err = os.Rename(tempFile.Name(), fileName); err != nil { if err = os.Rename(tempFile.Name(), fileName); err != nil {
return return

View File

@ -44,7 +44,7 @@ type VerifyRequest struct {
bpj []byte // "bytes payload, JSON" (BP + "}") bpj []byte // "bytes payload, JSON" (BP + "}")
bs []byte // "bytes signature", "{" + separator + camliSig, valid JSON bs []byte // "bytes signature", "{" + separator + camliSig, valid JSON
CamliSigner blobref.BlobRef CamliSigner *blobref.BlobRef
PayloadMap map[string]interface{} // The JSON values from BPJ PayloadMap map[string]interface{} // The JSON values from BPJ
CamliSig string CamliSig string

View File

@ -17,7 +17,7 @@ var flagPubKeyDir *string = flag.String("pubkey-dir", "test/pubkey-blobs",
"Temporary development hack; directory to dig-xxxx.camli public keys.") "Temporary development hack; directory to dig-xxxx.camli public keys.")
type pubkeyDirFetcher struct{} type pubkeyDirFetcher struct{}
func (_ *pubkeyDirFetcher) Fetch(b blobref.BlobRef) (file blobref.ReadSeekCloser, size int64, err os.Error) { func (_ *pubkeyDirFetcher) Fetch(b *blobref.BlobRef) (file blobref.ReadSeekCloser, size int64, err os.Error) {
fileName := fmt.Sprintf("%s/%s.camli", *flagPubKeyDir, b.String()) fileName := fmt.Sprintf("%s/%s.camli", *flagPubKeyDir, b.String())
var stat *os.FileInfo var stat *os.FileInfo
stat, err = os.Stat(fileName) stat, err = os.Stat(fileName)

View File

@ -30,12 +30,13 @@ ok($impl->start, "Server started");
$impl->verify_no_blobs; # also tests some of enumerate $impl->verify_no_blobs; # also tests some of enumerate
$impl->test_preupload_and_upload; $impl->test_preupload_and_upload;
$impl->test_upload_corrupt_blob; # blobref digest doesn't match
# upload a malicious blob (doesn't match sha1), verify it's rejected. # TODO: test multiple uploads in a batch
# test multiple uploads in a batch # TODO: test uploads in serial (using each response's next uploadUrl)
# test uploads in serial # TODO: test enumerate boundaries
# test enumerate boundaries # TODO: interrupt a POST upload in the middle; verify no straggler on
# interrupt a POST upload in the middle; verify no straggler on disk in subsequent GET # disk in subsequent GET
# .... # ....
# test auth works on bogus password? (auth still undefined) # test auth works on bogus password? (auth still undefined)
@ -104,11 +105,15 @@ sub ua {
return ($self->{_ua} ||= LWP::UserAgent->new(agent => "camli/blobserver-tester")); return ($self->{_ua} ||= LWP::UserAgent->new(agent => "camli/blobserver-tester"));
} }
sub root {
my $self= shift;
return $self->{root} or die "No 'root' for $self";
}
sub path { sub path {
my $self = shift; my $self = shift;
my $path = shift || ""; my $path = shift || "";
my $root = $self->{root} or die "No 'root' for $self"; return $self->root . $path;
return "$root$path";
} }
sub get_json { sub get_json {
@ -195,7 +200,7 @@ sub test_preupload_and_upload {
is(ref($already), "ARRAY", "alreadyHave is an array"); is(ref($already), "ARRAY", "alreadyHave is an array");
is(scalar(@$already), 0, "server doesn't have this blob yet."); is(scalar(@$already), 0, "server doesn't have this blob yet.");
like($jres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric"); like($jres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric");
my $upload_url = URI::URL->new($jres->{uploadUrl}); my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs;
ok($upload_url, "valid uploadUrl"); ok($upload_url, "valid uploadUrl");
# TODO: test & clarify in spec: are relative URLs allowed in uploadUrl? # TODO: test & clarify in spec: are relative URLs allowed in uploadUrl?
# App Engine seems to do it already, and makes it easier, so probably # App Engine seems to do it already, and makes it easier, so probably
@ -222,6 +227,37 @@ sub test_preupload_and_upload {
is(scalar(@$got), 1, "got one file"); is(scalar(@$got), 1, "got one file");
is($got->[0]{blobRef}, $blobref, "received[0] 'blobRef' matches"); is($got->[0]{blobRef}, $blobref, "received[0] 'blobRef' matches");
is($got->[0]{size}, length($blob), "received[0] 'size' matches"); is($got->[0]{size}, length($blob), "received[0] 'size' matches");
# TODO: do a get request, verify that we get it back.
}
sub test_upload_corrupt_blob {
my $self = shift;
my ($req, $res);
my $blob = "A blob, pre-corruption.";
my $blobref = "sha1-" . sha1_hex($blob);
$blob .= "OIEWUROIEWURLKJDSLKj CORRUPT";
$req = $self->post("/camli/preupload", {
"camliversion" => 1,
"blob1" => $blobref,
});
my $jres = $self->get_json($req, "valid preupload");
my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs;
# TODO: test & clarify in spec: are relative URLs allowed in uploadUrl?
# App Engine seems to do it already, and makes it easier, so probably
# best to clarify that they're relative.
# Do the actual upload
my $upreq = $self->upload_request($upload_url, {
$blobref => $blob,
});
diag("corrupt upload request: " . $upreq->as_string);
my $upres = $self->get_upload_json($upreq);
my $got = $upres->{received};
is(ref($got), "ARRAY", "corrupt upload returned a 'received' array");
is(scalar(@$got), 0, "didn't get any files (it was corrupt)");
} }
package Impl::Go; package Impl::Go;