diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index b4c91fa71..86863f257 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -57,6 +57,12 @@ const ( AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date" AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold" + // S3 conditional headers + IfMatch = "If-Match" + IfNoneMatch = "If-None-Match" + IfModifiedSince = "If-Modified-Since" + IfUnmodifiedSince = "If-Unmodified-Since" + // S3 conditional copy headers AmzCopySourceIfMatch = "X-Amz-Copy-Source-If-Match" AmzCopySourceIfNoneMatch = "X-Amz-Copy-Source-If-None-Match" diff --git a/weed/s3api/s3api_conditional_headers_test.go b/weed/s3api/s3api_conditional_headers_test.go new file mode 100644 index 000000000..bdc885472 --- /dev/null +++ b/weed/s3api/s3api_conditional_headers_test.go @@ -0,0 +1,849 @@ +package s3api + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" +) + +// TestConditionalHeadersWithExistingObjects tests conditional headers against existing objects +// This addresses the PR feedback about missing test coverage for object existence scenarios +func TestConditionalHeadersWithExistingObjects(t *testing.T) { + bucket := "test-bucket" + object := "/test-object" + + // Mock object with known ETag and modification time + testObject := &filer_pb.Entry{ + Name: "test-object", + Extended: map[string][]byte{ + s3_constants.ExtETagKey: []byte("\"abc123\""), + }, + Attributes: &filer_pb.FuseAttributes{ + Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), // June 15, 2024 + FileSize: 1024, // Add file size + }, + Chunks: []*filer_pb.FileChunk{ + // Add a mock chunk to make calculateETagFromChunks work + { + FileId: "test-file-id", + Offset: 0, + Size: 1024, + }, + }, + } + + // Test If-None-Match with existing object + t.Run("IfNoneMatch_ObjectExists", func(t *testing.T) { + // Test case 1: If-None-Match=* when object exists (should fail) + t.Run("Asterisk_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode) + } + }) + + // Test case 2: If-None-Match with matching ETag (should fail) + t.Run("MatchingETag_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "\"abc123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when ETag matches, got %v", errCode) + } + }) + + // Test case 3: If-None-Match with non-matching ETag (should succeed) + t.Run("NonMatchingETag_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when ETag doesn't match, got %v", errCode) + } + }) + + // Test case 4: If-None-Match with multiple ETags, one matching (should fail) + t.Run("MultipleETags_OneMatches_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"abc123\", \"def456\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when one ETag matches, got %v", errCode) + } + }) + + // Test case 5: If-None-Match with multiple ETags, none matching (should succeed) + t.Run("MultipleETags_NoneMatch_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"def456\", \"ghi123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when no ETags match, got %v", errCode) + } + }) + }) + + // Test If-Match with existing object + t.Run("IfMatch_ObjectExists", func(t *testing.T) { + // Test case 1: If-Match with matching ETag (should succeed) + t.Run("MatchingETag_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "\"abc123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when ETag matches, got %v", errCode) + } + }) + + // Test case 2: If-Match with non-matching ETag (should fail) + t.Run("NonMatchingETag_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "\"xyz789\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when ETag doesn't match, got %v", errCode) + } + }) + + // Test case 3: If-Match with multiple ETags, one matching (should succeed) + t.Run("MultipleETags_OneMatches_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "\"xyz789\", \"abc123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when one ETag matches, got %v", errCode) + } + }) + + // Test case 4: If-Match with wildcard * (should succeed if object exists) + t.Run("Wildcard_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when If-Match=* and object exists, got %v", errCode) + } + }) + }) + + // Test If-Modified-Since with existing object + t.Run("IfModifiedSince_ObjectExists", func(t *testing.T) { + // Test case 1: If-Modified-Since with date before object modification (should succeed) + t.Run("DateBefore_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC) + req.Header.Set(s3_constants.IfModifiedSince, dateBeforeModification.Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object was modified after date, got %v", errCode) + } + }) + + // Test case 2: If-Modified-Since with date after object modification (should fail) + t.Run("DateAfter_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC) + req.Header.Set(s3_constants.IfModifiedSince, dateAfterModification.Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object wasn't modified since date, got %v", errCode) + } + }) + + // Test case 3: If-Modified-Since with exact modification date (should fail - not after) + t.Run("ExactDate_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + exactDate := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) + req.Header.Set(s3_constants.IfModifiedSince, exactDate.Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object modification time equals header date, got %v", errCode) + } + }) + }) + + // Test If-Unmodified-Since with existing object + t.Run("IfUnmodifiedSince_ObjectExists", func(t *testing.T) { + // Test case 1: If-Unmodified-Since with date after object modification (should succeed) + t.Run("DateAfter_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC) + req.Header.Set(s3_constants.IfUnmodifiedSince, dateAfterModification.Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object wasn't modified after date, got %v", errCode) + } + }) + + // Test case 2: If-Unmodified-Since with date before object modification (should fail) + t.Run("DateBefore_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(testObject) + req := createTestPutRequest(bucket, object, "test content") + dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC) + req.Header.Set(s3_constants.IfUnmodifiedSince, dateBeforeModification.Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object was modified after date, got %v", errCode) + } + }) + }) +} + +// TestConditionalHeadersForReads tests conditional headers for read operations (GET, HEAD) +// This implements AWS S3 conditional reads behavior where different conditions return different status codes +// See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-reads.html +func TestConditionalHeadersForReads(t *testing.T) { + bucket := "test-bucket" + object := "/test-read-object" + + // Mock existing object to test conditional headers against + existingObject := &filer_pb.Entry{ + Name: "test-read-object", + Extended: map[string][]byte{ + s3_constants.ExtETagKey: []byte("\"read123\""), + }, + Attributes: &filer_pb.FuseAttributes{ + Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), + FileSize: 1024, + }, + Chunks: []*filer_pb.FileChunk{ + { + FileId: "read-file-id", + Offset: 0, + Size: 1024, + }, + }, + } + + // Test conditional reads with existing object + t.Run("ConditionalReads_ObjectExists", func(t *testing.T) { + // Test If-None-Match with existing object (should return 304 Not Modified) + t.Run("IfNoneMatch_ObjectExists_ShouldReturn304", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfNoneMatch, "\"read123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNotModified { + t.Errorf("Expected ErrNotModified when If-None-Match matches, got %v", errCode) + } + }) + + // Test If-None-Match=* with existing object (should return 304 Not Modified) + t.Run("IfNoneMatchAsterisk_ObjectExists_ShouldReturn304", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfNoneMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNotModified { + t.Errorf("Expected ErrNotModified when If-None-Match=* with existing object, got %v", errCode) + } + }) + + // Test If-None-Match with non-matching ETag (should succeed) + t.Run("IfNoneMatch_NonMatchingETag_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfNoneMatch, "\"different-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when If-None-Match doesn't match, got %v", errCode) + } + }) + + // Test If-Match with matching ETag (should succeed) + t.Run("IfMatch_MatchingETag_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfMatch, "\"read123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when If-Match matches, got %v", errCode) + } + }) + + // Test If-Match with non-matching ETag (should return 412 Precondition Failed) + t.Run("IfMatch_NonMatchingETag_ShouldReturn412", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfMatch, "\"different-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when If-Match doesn't match, got %v", errCode) + } + }) + + // Test If-Match=* with existing object (should succeed) + t.Run("IfMatchAsterisk_ObjectExists_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when If-Match=* with existing object, got %v", errCode) + } + }) + + // Test If-Modified-Since (object modified after date - should succeed) + t.Run("IfModifiedSince_ObjectModifiedAfter_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfModifiedSince, "Sat, 14 Jun 2024 12:00:00 GMT") // Before object mtime + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object modified after If-Modified-Since date, got %v", errCode) + } + }) + + // Test If-Modified-Since (object not modified since date - should return 304) + t.Run("IfModifiedSince_ObjectNotModified_ShouldReturn304", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfModifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNotModified { + t.Errorf("Expected ErrNotModified when object not modified since If-Modified-Since date, got %v", errCode) + } + }) + + // Test If-Unmodified-Since (object not modified since date - should succeed) + t.Run("IfUnmodifiedSince_ObjectNotModified_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfUnmodifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object not modified since If-Unmodified-Since date, got %v", errCode) + } + }) + + // Test If-Unmodified-Since (object modified since date - should return 412) + t.Run("IfUnmodifiedSince_ObjectModified_ShouldReturn412", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfUnmodifiedSince, "Fri, 14 Jun 2024 12:00:00 GMT") // Before object mtime + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object modified since If-Unmodified-Since date, got %v", errCode) + } + }) + }) + + // Test conditional reads with non-existent object + t.Run("ConditionalReads_ObjectNotExists", func(t *testing.T) { + // Test If-None-Match with non-existent object (should succeed) + t.Run("IfNoneMatch_ObjectNotExists_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfNoneMatch, "\"any-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match, got %v", errCode) + } + }) + + // Test If-Match with non-existent object (should return 412) + t.Run("IfMatch_ObjectNotExists_ShouldReturn412", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfMatch, "\"any-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode) + } + }) + + // Test If-Modified-Since with non-existent object (should succeed) + t.Run("IfModifiedSince_ObjectNotExists_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfModifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object doesn't exist with If-Modified-Since, got %v", errCode) + } + }) + + // Test If-Unmodified-Since with non-existent object (should return 412) + t.Run("IfUnmodifiedSince_ObjectNotExists_ShouldReturn412", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object + + req := createTestGetRequest(bucket, object) + req.Header.Set(s3_constants.IfUnmodifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Unmodified-Since, got %v", errCode) + } + }) + }) +} + +// Helper function to create a GET request for testing +func createTestGetRequest(bucket, object string) *http.Request { + return &http.Request{ + Method: "GET", + Header: make(http.Header), + URL: &url.URL{ + Path: fmt.Sprintf("/%s%s", bucket, object), + }, + } +} + +// TestConditionalHeadersWithNonExistentObjects tests the original scenarios (object doesn't exist) +func TestConditionalHeadersWithNonExistentObjects(t *testing.T) { + s3a := NewS3ApiServerForTest() + if s3a == nil { + t.Skip("S3ApiServer not available for testing") + } + + bucket := "test-bucket" + object := "/test-object" + + // Test If-None-Match header when object doesn't exist + t.Run("IfNoneMatch_ObjectDoesNotExist", func(t *testing.T) { + // Test case 1: If-None-Match=* when object doesn't exist (should return ErrNone) + t.Run("Asterisk_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode) + } + }) + + // Test case 2: If-None-Match with specific ETag when object doesn't exist + t.Run("SpecificETag_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfNoneMatch, "\"some-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode) + } + }) + }) + + // Test If-Match header when object doesn't exist + t.Run("IfMatch_ObjectDoesNotExist", func(t *testing.T) { + // Test case 1: If-Match with specific ETag when object doesn't exist (should fail - critical bug fix) + t.Run("SpecificETag_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "\"some-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match header, got %v", errCode) + } + }) + + // Test case 2: If-Match with wildcard * when object doesn't exist (should fail) + t.Run("Wildcard_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match=*, got %v", errCode) + } + }) + }) + + // Test date format validation (works regardless of object existence) + t.Run("DateFormatValidation", func(t *testing.T) { + // Test case 1: Valid If-Modified-Since date format + t.Run("IfModifiedSince_ValidFormat", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfModifiedSince, time.Now().Format(time.RFC1123)) + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone with valid date format, got %v", errCode) + } + }) + + // Test case 2: Invalid If-Modified-Since date format + t.Run("IfModifiedSince_InvalidFormat", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfModifiedSince, "invalid-date") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrInvalidRequest { + t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode) + } + }) + + // Test case 3: Invalid If-Unmodified-Since date format + t.Run("IfUnmodifiedSince_InvalidFormat", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + req.Header.Set(s3_constants.IfUnmodifiedSince, "invalid-date") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrInvalidRequest { + t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode) + } + }) + }) + + // Test no conditional headers + t.Run("NoConditionalHeaders", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No object exists + req := createTestPutRequest(bucket, object, "test content") + // Don't set any conditional headers + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when no conditional headers, got %v", errCode) + } + }) +} + +// TestETagMatching tests the etagMatches helper function +func TestETagMatching(t *testing.T) { + s3a := NewS3ApiServerForTest() + if s3a == nil { + t.Skip("S3ApiServer not available for testing") + } + + testCases := []struct { + name string + headerValue string + objectETag string + expected bool + }{ + { + name: "ExactMatch", + headerValue: "\"abc123\"", + objectETag: "abc123", + expected: true, + }, + { + name: "ExactMatchWithQuotes", + headerValue: "\"abc123\"", + objectETag: "\"abc123\"", + expected: true, + }, + { + name: "NoMatch", + headerValue: "\"abc123\"", + objectETag: "def456", + expected: false, + }, + { + name: "MultipleETags_FirstMatch", + headerValue: "\"abc123\", \"def456\"", + objectETag: "abc123", + expected: true, + }, + { + name: "MultipleETags_SecondMatch", + headerValue: "\"abc123\", \"def456\"", + objectETag: "def456", + expected: true, + }, + { + name: "MultipleETags_NoMatch", + headerValue: "\"abc123\", \"def456\"", + objectETag: "ghi789", + expected: false, + }, + { + name: "WithSpaces", + headerValue: " \"abc123\" , \"def456\" ", + objectETag: "def456", + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := s3a.etagMatches(tc.headerValue, tc.objectETag) + if result != tc.expected { + t.Errorf("Expected %v, got %v for headerValue='%s', objectETag='%s'", + tc.expected, result, tc.headerValue, tc.objectETag) + } + }) + } +} + +// TestConditionalHeadersIntegration tests conditional headers with full integration +func TestConditionalHeadersIntegration(t *testing.T) { + // This would be a full integration test that requires a running SeaweedFS instance + t.Skip("Integration test - requires running SeaweedFS instance") +} + +// createTestPutRequest creates a test HTTP PUT request +func createTestPutRequest(bucket, object, content string) *http.Request { + req, _ := http.NewRequest("PUT", "/"+bucket+object, bytes.NewReader([]byte(content))) + req.Header.Set("Content-Type", "application/octet-stream") + + // Set up mux vars to simulate the bucket and object extraction + // In real tests, this would be handled by the gorilla mux router + return req +} + +// NewS3ApiServerForTest creates a minimal S3ApiServer for testing +// Note: This is a simplified version for unit testing conditional logic +func NewS3ApiServerForTest() *S3ApiServer { + // In a real test environment, this would set up a proper S3ApiServer + // with filer connection, etc. For unit testing conditional header logic, + // we create a minimal instance + return &S3ApiServer{ + option: &S3ApiServerOption{ + BucketsPath: "/buckets", + }, + } +} + +// MockEntryGetter implements the simplified EntryGetter interface for testing +// Only mocks the data access dependency - tests use production getObjectETag and etagMatches +type MockEntryGetter struct { + mockEntry *filer_pb.Entry +} + +// Implement only the simplified EntryGetter interface +func (m *MockEntryGetter) getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error) { + if m.mockEntry != nil { + return m.mockEntry, nil + } + return nil, filer_pb.ErrNotFound +} + +// createMockEntryGetter creates a mock EntryGetter for testing +func createMockEntryGetter(mockEntry *filer_pb.Entry) *MockEntryGetter { + return &MockEntryGetter{ + mockEntry: mockEntry, + } +} + +// TestConditionalHeadersMultipartUpload tests conditional headers with multipart uploads +// This verifies AWS S3 compatibility where conditional headers only apply to CompleteMultipartUpload +func TestConditionalHeadersMultipartUpload(t *testing.T) { + bucket := "test-bucket" + object := "/test-multipart-object" + + // Mock existing object to test conditional headers against + existingObject := &filer_pb.Entry{ + Name: "test-multipart-object", + Extended: map[string][]byte{ + s3_constants.ExtETagKey: []byte("\"existing123\""), + }, + Attributes: &filer_pb.FuseAttributes{ + Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), + FileSize: 2048, + }, + Chunks: []*filer_pb.FileChunk{ + { + FileId: "existing-file-id", + Offset: 0, + Size: 2048, + }, + }, + } + + // Test CompleteMultipartUpload with If-None-Match: * (should fail when object exists) + t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectExists_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + // Create a mock CompleteMultipartUpload request with If-None-Match: * + req := &http.Request{ + Method: "POST", + Header: make(http.Header), + URL: &url.URL{ + RawQuery: "uploadId=test-upload-id", + }, + } + req.Header.Set(s3_constants.IfNoneMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode) + } + }) + + // Test CompleteMultipartUpload with If-None-Match: * (should succeed when object doesn't exist) + t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectNotExists_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No existing object + + req := &http.Request{ + Method: "POST", + Header: make(http.Header), + URL: &url.URL{ + RawQuery: "uploadId=test-upload-id", + }, + } + req.Header.Set(s3_constants.IfNoneMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match=*, got %v", errCode) + } + }) + + // Test CompleteMultipartUpload with If-Match (should succeed when ETag matches) + t.Run("CompleteMultipartUpload_IfMatch_ETagMatches_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := &http.Request{ + Method: "POST", + Header: make(http.Header), + URL: &url.URL{ + RawQuery: "uploadId=test-upload-id", + }, + } + req.Header.Set(s3_constants.IfMatch, "\"existing123\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when ETag matches, got %v", errCode) + } + }) + + // Test CompleteMultipartUpload with If-Match (should fail when object doesn't exist) + t.Run("CompleteMultipartUpload_IfMatch_ObjectNotExists_ShouldFail", func(t *testing.T) { + getter := createMockEntryGetter(nil) // No existing object + + req := &http.Request{ + Method: "POST", + Header: make(http.Header), + URL: &url.URL{ + RawQuery: "uploadId=test-upload-id", + }, + } + req.Header.Set(s3_constants.IfMatch, "\"any-etag\"") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrPreconditionFailed { + t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode) + } + }) + + // Test CompleteMultipartUpload with If-Match wildcard (should succeed when object exists) + t.Run("CompleteMultipartUpload_IfMatchWildcard_ObjectExists_ShouldSucceed", func(t *testing.T) { + getter := createMockEntryGetter(existingObject) + + req := &http.Request{ + Method: "POST", + Header: make(http.Header), + URL: &url.URL{ + RawQuery: "uploadId=test-upload-id", + }, + } + req.Header.Set(s3_constants.IfMatch, "*") + + s3a := NewS3ApiServerForTest() + errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object) + if errCode != s3err.ErrNone { + t.Errorf("Expected ErrNone when object exists with If-Match=*, got %v", errCode) + } + }) +} diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 25647538b..5da88bf77 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -246,6 +246,13 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) return // Directory object request was handled } + // Check conditional headers for read operations + if errCode := s3a.checkConditionalHeadersForReads(r, bucket, object); errCode != s3err.ErrNone { + glog.V(3).Infof("GetObjectHandler: Conditional header check failed for %s/%s with error %v", bucket, object, errCode) + s3err.WriteErrorResponse(w, r, errCode) + return + } + // Check for specific version ID in query parameters versionId := r.URL.Query().Get("versionId") @@ -378,6 +385,13 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request return // Directory object request was handled } + // Check conditional headers for read operations + if errCode := s3a.checkConditionalHeadersForReads(r, bucket, object); errCode != s3err.ErrNone { + glog.V(3).Infof("HeadObjectHandler: Conditional header check failed for %s/%s with error %v", bucket, object, errCode) + s3err.WriteErrorResponse(w, r, errCode) + return + } + // Check for specific version ID in query parameters versionId := r.URL.Query().Get("versionId") diff --git a/weed/s3api/s3api_object_handlers_multipart.go b/weed/s3api/s3api_object_handlers_multipart.go index cee8f6785..3d83b585b 100644 --- a/weed/s3api/s3api_object_handlers_multipart.go +++ b/weed/s3api/s3api_object_handlers_multipart.go @@ -114,6 +114,14 @@ func (s3a *S3ApiServer) CompleteMultipartUploadHandler(w http.ResponseWriter, r return } + // Check conditional headers before completing multipart upload + // This implements AWS S3 behavior where conditional headers apply to CompleteMultipartUpload + if errCode := s3a.checkConditionalHeaders(r, bucket, object); errCode != s3err.ErrNone { + glog.V(3).Infof("CompleteMultipartUploadHandler: Conditional header check failed for %s/%s", bucket, object) + s3err.WriteErrorResponse(w, r, errCode) + return + } + response, errCode := s3a.completeMultipartUpload(r, &s3.CompleteMultipartUploadInput{ Bucket: aws.String(bucket), Key: objectKey(aws.String(object)), diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 18cd08c37..8a3362d5a 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -72,6 +72,12 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) return } + // Check conditional headers + if errCode := s3a.checkConditionalHeaders(r, bucket, object); errCode != s3err.ErrNone { + s3err.WriteErrorResponse(w, r, errCode) + return + } + if r.Header.Get("Cache-Control") != "" { if _, err = cacheobject.ParseRequestCacheControl(r.Header.Get("Cache-Control")); err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrInvalidDigest) @@ -1012,3 +1018,269 @@ func mapValidationErrorToS3Error(err error) s3err.ErrorCode { return s3err.ErrInvalidRequest } + +// EntryGetter interface for dependency injection in tests +// Simplified to only mock the data access dependency +type EntryGetter interface { + getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error) +} + +// conditionalHeaders holds parsed conditional header values +type conditionalHeaders struct { + ifMatch string + ifNoneMatch string + ifModifiedSince time.Time + ifUnmodifiedSince time.Time + isSet bool // true if any conditional headers are present +} + +// parseConditionalHeaders extracts and validates conditional headers from the request +func parseConditionalHeaders(r *http.Request) (conditionalHeaders, s3err.ErrorCode) { + headers := conditionalHeaders{ + ifMatch: r.Header.Get(s3_constants.IfMatch), + ifNoneMatch: r.Header.Get(s3_constants.IfNoneMatch), + } + + ifModifiedSinceStr := r.Header.Get(s3_constants.IfModifiedSince) + ifUnmodifiedSinceStr := r.Header.Get(s3_constants.IfUnmodifiedSince) + + // Check if any conditional headers are present + headers.isSet = headers.ifMatch != "" || headers.ifNoneMatch != "" || + ifModifiedSinceStr != "" || ifUnmodifiedSinceStr != "" + + if !headers.isSet { + return headers, s3err.ErrNone + } + + // Parse date headers with validation + var err error + if ifModifiedSinceStr != "" { + headers.ifModifiedSince, err = time.Parse(time.RFC1123, ifModifiedSinceStr) + if err != nil { + glog.V(3).Infof("parseConditionalHeaders: Invalid If-Modified-Since format: %v", err) + return headers, s3err.ErrInvalidRequest + } + } + + if ifUnmodifiedSinceStr != "" { + headers.ifUnmodifiedSince, err = time.Parse(time.RFC1123, ifUnmodifiedSinceStr) + if err != nil { + glog.V(3).Infof("parseConditionalHeaders: Invalid If-Unmodified-Since format: %v", err) + return headers, s3err.ErrInvalidRequest + } + } + + return headers, s3err.ErrNone +} + +// S3ApiServer implements EntryGetter interface +func (s3a *S3ApiServer) getObjectETag(entry *filer_pb.Entry) string { + // Try to get ETag from Extended attributes first + if etagBytes, hasETag := entry.Extended[s3_constants.ExtETagKey]; hasETag { + return string(etagBytes) + } + // Fallback: calculate ETag from chunks + return s3a.calculateETagFromChunks(entry.Chunks) +} + +func (s3a *S3ApiServer) etagMatches(headerValue, objectETag string) bool { + // Clean the object ETag + objectETag = strings.Trim(objectETag, `"`) + + // Split header value by commas to handle multiple ETags + etags := strings.Split(headerValue, ",") + for _, etag := range etags { + etag = strings.TrimSpace(etag) + etag = strings.Trim(etag, `"`) + if etag == objectETag { + return true + } + } + return false +} + +// checkConditionalHeadersWithGetter is a testable method that accepts a simple EntryGetter +// Uses the production getObjectETag and etagMatches methods to ensure testing of real logic +func (s3a *S3ApiServer) checkConditionalHeadersWithGetter(getter EntryGetter, r *http.Request, bucket, object string) s3err.ErrorCode { + headers, errCode := parseConditionalHeaders(r) + if errCode != s3err.ErrNone { + glog.V(3).Infof("checkConditionalHeaders: Invalid date format") + return errCode + } + if !headers.isSet { + return s3err.ErrNone + } + + // Get object entry for conditional checks. + bucketDir := "/buckets/" + bucket + entry, entryErr := getter.getEntry(bucketDir, object) + objectExists := entryErr == nil + + // For PUT requests, all specified conditions must be met. + // The evaluation order follows AWS S3 behavior for consistency. + + // 1. Check If-Match + if headers.ifMatch != "" { + if !objectExists { + glog.V(3).Infof("checkConditionalHeaders: If-Match failed - object %s/%s does not exist", bucket, object) + return s3err.ErrPreconditionFailed + } + // If `ifMatch` is "*", the condition is met if the object exists. + // Otherwise, we need to check the ETag. + if headers.ifMatch != "*" { + // Use production getObjectETag method + objectETag := s3a.getObjectETag(entry) + // Use production etagMatches method + if !s3a.etagMatches(headers.ifMatch, objectETag) { + glog.V(3).Infof("checkConditionalHeaders: If-Match failed for object %s/%s - expected ETag %s, got %s", bucket, object, headers.ifMatch, objectETag) + return s3err.ErrPreconditionFailed + } + } + glog.V(3).Infof("checkConditionalHeaders: If-Match passed for object %s/%s", bucket, object) + } + + // 2. Check If-Unmodified-Since + if !headers.ifUnmodifiedSince.IsZero() { + if objectExists { + objectModTime := time.Unix(entry.Attributes.Mtime, 0) + if objectModTime.After(headers.ifUnmodifiedSince) { + glog.V(3).Infof("checkConditionalHeaders: If-Unmodified-Since failed - object modified after %s", r.Header.Get(s3_constants.IfUnmodifiedSince)) + return s3err.ErrPreconditionFailed + } + glog.V(3).Infof("checkConditionalHeaders: If-Unmodified-Since passed - object not modified since %s", r.Header.Get(s3_constants.IfUnmodifiedSince)) + } + } + + // 3. Check If-None-Match + if headers.ifNoneMatch != "" { + if objectExists { + if headers.ifNoneMatch == "*" { + glog.V(3).Infof("checkConditionalHeaders: If-None-Match=* failed - object %s/%s exists", bucket, object) + return s3err.ErrPreconditionFailed + } + // Use production getObjectETag method + objectETag := s3a.getObjectETag(entry) + // Use production etagMatches method + if s3a.etagMatches(headers.ifNoneMatch, objectETag) { + glog.V(3).Infof("checkConditionalHeaders: If-None-Match failed - ETag matches %s", objectETag) + return s3err.ErrPreconditionFailed + } + glog.V(3).Infof("checkConditionalHeaders: If-None-Match passed - ETag %s doesn't match %s", objectETag, headers.ifNoneMatch) + } else { + glog.V(3).Infof("checkConditionalHeaders: If-None-Match passed - object %s/%s does not exist", bucket, object) + } + } + + // 4. Check If-Modified-Since + if !headers.ifModifiedSince.IsZero() { + if objectExists { + objectModTime := time.Unix(entry.Attributes.Mtime, 0) + if !objectModTime.After(headers.ifModifiedSince) { + glog.V(3).Infof("checkConditionalHeaders: If-Modified-Since failed - object not modified since %s", r.Header.Get(s3_constants.IfModifiedSince)) + return s3err.ErrPreconditionFailed + } + glog.V(3).Infof("checkConditionalHeaders: If-Modified-Since passed - object modified after %s", r.Header.Get(s3_constants.IfModifiedSince)) + } + } + + return s3err.ErrNone +} + +// checkConditionalHeaders is the production method that uses the S3ApiServer as EntryGetter +func (s3a *S3ApiServer) checkConditionalHeaders(r *http.Request, bucket, object string) s3err.ErrorCode { + return s3a.checkConditionalHeadersWithGetter(s3a, r, bucket, object) +} + +// checkConditionalHeadersForReadsWithGetter is a testable method for read operations +// Uses the production getObjectETag and etagMatches methods to ensure testing of real logic +func (s3a *S3ApiServer) checkConditionalHeadersForReadsWithGetter(getter EntryGetter, r *http.Request, bucket, object string) s3err.ErrorCode { + headers, errCode := parseConditionalHeaders(r) + if errCode != s3err.ErrNone { + glog.V(3).Infof("checkConditionalHeadersForReads: Invalid date format") + return errCode + } + if !headers.isSet { + return s3err.ErrNone + } + + // Get object entry for conditional checks. + bucketDir := "/buckets/" + bucket + entry, entryErr := getter.getEntry(bucketDir, object) + objectExists := entryErr == nil + + // If object doesn't exist, fail for If-Match and If-Unmodified-Since + if !objectExists { + if headers.ifMatch != "" { + glog.V(3).Infof("checkConditionalHeadersForReads: If-Match failed - object %s/%s does not exist", bucket, object) + return s3err.ErrPreconditionFailed + } + if !headers.ifUnmodifiedSince.IsZero() { + glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since failed - object %s/%s does not exist", bucket, object) + return s3err.ErrPreconditionFailed + } + // If-None-Match and If-Modified-Since succeed when object doesn't exist + return s3err.ErrNone + } + + // Object exists - check all conditions + // The evaluation order follows AWS S3 behavior for consistency. + + // 1. Check If-Match (412 Precondition Failed if fails) + if headers.ifMatch != "" { + // If `ifMatch` is "*", the condition is met if the object exists. + // Otherwise, we need to check the ETag. + if headers.ifMatch != "*" { + // Use production getObjectETag method + objectETag := s3a.getObjectETag(entry) + // Use production etagMatches method + if !s3a.etagMatches(headers.ifMatch, objectETag) { + glog.V(3).Infof("checkConditionalHeadersForReads: If-Match failed for object %s/%s - expected ETag %s, got %s", bucket, object, headers.ifMatch, objectETag) + return s3err.ErrPreconditionFailed + } + } + glog.V(3).Infof("checkConditionalHeadersForReads: If-Match passed for object %s/%s", bucket, object) + } + + // 2. Check If-Unmodified-Since (412 Precondition Failed if fails) + if !headers.ifUnmodifiedSince.IsZero() { + objectModTime := time.Unix(entry.Attributes.Mtime, 0) + if objectModTime.After(headers.ifUnmodifiedSince) { + glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since failed - object modified after %s", r.Header.Get(s3_constants.IfUnmodifiedSince)) + return s3err.ErrPreconditionFailed + } + glog.V(3).Infof("checkConditionalHeadersForReads: If-Unmodified-Since passed - object not modified since %s", r.Header.Get(s3_constants.IfUnmodifiedSince)) + } + + // 3. Check If-None-Match (304 Not Modified if fails) + if headers.ifNoneMatch != "" { + if headers.ifNoneMatch == "*" { + glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match=* failed - object %s/%s exists", bucket, object) + return s3err.ErrNotModified + } + // Use production getObjectETag method + objectETag := s3a.getObjectETag(entry) + // Use production etagMatches method + if s3a.etagMatches(headers.ifNoneMatch, objectETag) { + glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match failed - ETag matches %s", objectETag) + return s3err.ErrNotModified + } + glog.V(3).Infof("checkConditionalHeadersForReads: If-None-Match passed - ETag %s doesn't match %s", objectETag, headers.ifNoneMatch) + } + + // 4. Check If-Modified-Since (304 Not Modified if fails) + if !headers.ifModifiedSince.IsZero() { + objectModTime := time.Unix(entry.Attributes.Mtime, 0) + if !objectModTime.After(headers.ifModifiedSince) { + glog.V(3).Infof("checkConditionalHeadersForReads: If-Modified-Since failed - object not modified since %s", r.Header.Get(s3_constants.IfModifiedSince)) + return s3err.ErrNotModified + } + glog.V(3).Infof("checkConditionalHeadersForReads: If-Modified-Since passed - object modified after %s", r.Header.Get(s3_constants.IfModifiedSince)) + } + + return s3err.ErrNone +} + +// checkConditionalHeadersForReads is the production method that uses the S3ApiServer as EntryGetter +func (s3a *S3ApiServer) checkConditionalHeadersForReads(r *http.Request, bucket, object string) s3err.ErrorCode { + return s3a.checkConditionalHeadersForReadsWithGetter(s3a, r, bucket, object) +} diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go index 78ba8d2da..9cc343680 100644 --- a/weed/s3api/s3err/s3api_errors.go +++ b/weed/s3api/s3err/s3api_errors.go @@ -102,6 +102,7 @@ const ( ErrAuthNotSetup ErrNotImplemented ErrPreconditionFailed + ErrNotModified ErrExistingObjectIsDirectory ErrExistingObjectIsFile @@ -451,6 +452,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "At least one of the pre-conditions you specified did not hold", HTTPStatusCode: http.StatusPreconditionFailed, }, + ErrNotModified: { + Code: "NotModified", + Description: "The object was not modified since the specified time", + HTTPStatusCode: http.StatusNotModified, + }, ErrExistingObjectIsDirectory: { Code: "ExistingObjectIsDirectory", Description: "Existing Object is a directory.",