You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							401 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							401 lines
						
					
					
						
							11 KiB
						
					
					
				| package s3api | |
| 
 | |
| import ( | |
| 	"bytes" | |
| 	"net/http" | |
| 	"net/http/httptest" | |
| 	"testing" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" | |
| ) | |
| 
 | |
| // TestPutObjectWithSSEC tests PUT object with SSE-C through HTTP handler | |
| func TestPutObjectWithSSEC(t *testing.T) { | |
| 	keyPair := GenerateTestSSECKey(1) | |
| 	testData := "Hello, SSE-C PUT object!" | |
| 
 | |
| 	// Create HTTP request | |
| 	req := CreateTestHTTPRequest("PUT", "/test-bucket/test-object", []byte(testData)) | |
| 	SetupTestSSECHeaders(req, keyPair) | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "test-bucket", | |
| 		"object": "test-object", | |
| 	}) | |
| 
 | |
| 	// Create response recorder | |
| 	w := CreateTestHTTPResponse() | |
| 
 | |
| 	// Test header validation | |
| 	err := ValidateSSECHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Header validation failed: %v", err) | |
| 	} | |
| 
 | |
| 	// Parse SSE-C headers | |
| 	customerKey, err := ParseSSECHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to parse SSE-C headers: %v", err) | |
| 	} | |
| 
 | |
| 	if customerKey == nil { | |
| 		t.Fatal("Expected customer key, got nil") | |
| 	} | |
| 
 | |
| 	// Verify parsed key matches input | |
| 	if !bytes.Equal(customerKey.Key, keyPair.Key) { | |
| 		t.Error("Parsed key doesn't match input key") | |
| 	} | |
| 
 | |
| 	if customerKey.KeyMD5 != keyPair.KeyMD5 { | |
| 		t.Errorf("Parsed key MD5 doesn't match: expected %s, got %s", keyPair.KeyMD5, customerKey.KeyMD5) | |
| 	} | |
| 
 | |
| 	// Simulate setting response headers | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256") | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyPair.KeyMD5) | |
| 
 | |
| 	// Verify response headers | |
| 	AssertSSECHeaders(t, w, keyPair) | |
| } | |
| 
 | |
| // TestGetObjectWithSSEC tests GET object with SSE-C through HTTP handler | |
| func TestGetObjectWithSSEC(t *testing.T) { | |
| 	keyPair := GenerateTestSSECKey(1) | |
| 
 | |
| 	// Create HTTP request for GET | |
| 	req := CreateTestHTTPRequest("GET", "/test-bucket/test-object", nil) | |
| 	SetupTestSSECHeaders(req, keyPair) | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "test-bucket", | |
| 		"object": "test-object", | |
| 	}) | |
| 
 | |
| 	// Create response recorder | |
| 	w := CreateTestHTTPResponse() | |
| 
 | |
| 	// Test that SSE-C is detected for GET requests | |
| 	if !IsSSECRequest(req) { | |
| 		t.Error("Should detect SSE-C request for GET with SSE-C headers") | |
| 	} | |
| 
 | |
| 	// Validate headers | |
| 	err := ValidateSSECHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Header validation failed: %v", err) | |
| 	} | |
| 
 | |
| 	// Simulate response with SSE-C headers | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256") | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyPair.KeyMD5) | |
| 	w.WriteHeader(http.StatusOK) | |
| 
 | |
| 	// Verify response | |
| 	if w.Code != http.StatusOK { | |
| 		t.Errorf("Expected status 200, got %d", w.Code) | |
| 	} | |
| 
 | |
| 	AssertSSECHeaders(t, w, keyPair) | |
| } | |
| 
 | |
| // TestPutObjectWithSSEKMS tests PUT object with SSE-KMS through HTTP handler | |
| func TestPutObjectWithSSEKMS(t *testing.T) { | |
| 	kmsKey := SetupTestKMS(t) | |
| 	defer kmsKey.Cleanup() | |
| 
 | |
| 	testData := "Hello, SSE-KMS PUT object!" | |
| 
 | |
| 	// Create HTTP request | |
| 	req := CreateTestHTTPRequest("PUT", "/test-bucket/test-object", []byte(testData)) | |
| 	SetupTestSSEKMSHeaders(req, kmsKey.KeyID) | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "test-bucket", | |
| 		"object": "test-object", | |
| 	}) | |
| 
 | |
| 	// Create response recorder | |
| 	w := CreateTestHTTPResponse() | |
| 
 | |
| 	// Test that SSE-KMS is detected | |
| 	if !IsSSEKMSRequest(req) { | |
| 		t.Error("Should detect SSE-KMS request") | |
| 	} | |
| 
 | |
| 	// Parse SSE-KMS headers | |
| 	sseKmsKey, err := ParseSSEKMSHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to parse SSE-KMS headers: %v", err) | |
| 	} | |
| 
 | |
| 	if sseKmsKey == nil { | |
| 		t.Fatal("Expected SSE-KMS key, got nil") | |
| 	} | |
| 
 | |
| 	if sseKmsKey.KeyID != kmsKey.KeyID { | |
| 		t.Errorf("Parsed key ID doesn't match: expected %s, got %s", kmsKey.KeyID, sseKmsKey.KeyID) | |
| 	} | |
| 
 | |
| 	// Simulate setting response headers | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryption, "aws:kms") | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, kmsKey.KeyID) | |
| 
 | |
| 	// Verify response headers | |
| 	AssertSSEKMSHeaders(t, w, kmsKey.KeyID) | |
| } | |
| 
 | |
| // TestGetObjectWithSSEKMS tests GET object with SSE-KMS through HTTP handler | |
| func TestGetObjectWithSSEKMS(t *testing.T) { | |
| 	kmsKey := SetupTestKMS(t) | |
| 	defer kmsKey.Cleanup() | |
| 
 | |
| 	// Create HTTP request for GET (no SSE headers needed for GET) | |
| 	req := CreateTestHTTPRequest("GET", "/test-bucket/test-object", nil) | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "test-bucket", | |
| 		"object": "test-object", | |
| 	}) | |
| 
 | |
| 	// Create response recorder | |
| 	w := CreateTestHTTPResponse() | |
| 
 | |
| 	// Simulate response with SSE-KMS headers (would come from stored metadata) | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryption, "aws:kms") | |
| 	w.Header().Set(s3_constants.AmzServerSideEncryptionAwsKmsKeyId, kmsKey.KeyID) | |
| 	w.WriteHeader(http.StatusOK) | |
| 
 | |
| 	// Verify response | |
| 	if w.Code != http.StatusOK { | |
| 		t.Errorf("Expected status 200, got %d", w.Code) | |
| 	} | |
| 
 | |
| 	AssertSSEKMSHeaders(t, w, kmsKey.KeyID) | |
| } | |
| 
 | |
| // TestSSECRangeRequestSupport tests that range requests are now supported for SSE-C | |
| func TestSSECRangeRequestSupport(t *testing.T) { | |
| 	keyPair := GenerateTestSSECKey(1) | |
| 
 | |
| 	// Create HTTP request with Range header | |
| 	req := CreateTestHTTPRequest("GET", "/test-bucket/test-object", nil) | |
| 	req.Header.Set("Range", "bytes=0-100") | |
| 	SetupTestSSECHeaders(req, keyPair) | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "test-bucket", | |
| 		"object": "test-object", | |
| 	}) | |
| 
 | |
| 	// Create a mock proxy response with SSE-C headers | |
| 	proxyResponse := httptest.NewRecorder() | |
| 	proxyResponse.Header().Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256") | |
| 	proxyResponse.Header().Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, keyPair.KeyMD5) | |
| 	proxyResponse.Header().Set("Content-Length", "1000") | |
| 
 | |
| 	// Test the detection logic - these should all still work | |
|  | |
| 	// Should detect as SSE-C request | |
| 	if !IsSSECRequest(req) { | |
| 		t.Error("Should detect SSE-C request") | |
| 	} | |
| 
 | |
| 	// Should detect range request | |
| 	if req.Header.Get("Range") == "" { | |
| 		t.Error("Range header should be present") | |
| 	} | |
| 
 | |
| 	// The combination should now be allowed and handled by the filer layer | |
| 	// Range requests with SSE-C are now supported since IV is stored in metadata | |
| } | |
| 
 | |
| // TestSSEHeaderConflicts tests conflicting SSE headers | |
| func TestSSEHeaderConflicts(t *testing.T) { | |
| 	testCases := []struct { | |
| 		name    string | |
| 		setupFn func(*http.Request) | |
| 		valid   bool | |
| 	}{ | |
| 		{ | |
| 			name: "SSE-C and SSE-KMS conflict", | |
| 			setupFn: func(req *http.Request) { | |
| 				keyPair := GenerateTestSSECKey(1) | |
| 				SetupTestSSECHeaders(req, keyPair) | |
| 				SetupTestSSEKMSHeaders(req, "test-key-id") | |
| 			}, | |
| 			valid: false, | |
| 		}, | |
| 		{ | |
| 			name: "Valid SSE-C only", | |
| 			setupFn: func(req *http.Request) { | |
| 				keyPair := GenerateTestSSECKey(1) | |
| 				SetupTestSSECHeaders(req, keyPair) | |
| 			}, | |
| 			valid: true, | |
| 		}, | |
| 		{ | |
| 			name: "Valid SSE-KMS only", | |
| 			setupFn: func(req *http.Request) { | |
| 				SetupTestSSEKMSHeaders(req, "test-key-id") | |
| 			}, | |
| 			valid: true, | |
| 		}, | |
| 		{ | |
| 			name: "No SSE headers", | |
| 			setupFn: func(req *http.Request) { | |
| 				// No SSE headers | |
| 			}, | |
| 			valid: true, | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			req := CreateTestHTTPRequest("PUT", "/test-bucket/test-object", []byte("test")) | |
| 			tc.setupFn(req) | |
| 
 | |
| 			ssecDetected := IsSSECRequest(req) | |
| 			sseKmsDetected := IsSSEKMSRequest(req) | |
| 
 | |
| 			// Both shouldn't be detected simultaneously | |
| 			if ssecDetected && sseKmsDetected { | |
| 				t.Error("Both SSE-C and SSE-KMS should not be detected simultaneously") | |
| 			} | |
| 
 | |
| 			// Test validation if SSE-C is detected | |
| 			if ssecDetected { | |
| 				err := ValidateSSECHeaders(req) | |
| 				if tc.valid && err != nil { | |
| 					t.Errorf("Expected valid SSE-C headers, got error: %v", err) | |
| 				} | |
| 				if !tc.valid && err == nil && tc.name == "SSE-C and SSE-KMS conflict" { | |
| 					// This specific test case should probably be handled at a higher level | |
| 					t.Log("Conflict detection should be handled by higher-level validation") | |
| 				} | |
| 			} | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| // TestSSECopySourceHeaders tests copy operations with SSE headers | |
| func TestSSECopySourceHeaders(t *testing.T) { | |
| 	sourceKey := GenerateTestSSECKey(1) | |
| 	destKey := GenerateTestSSECKey(2) | |
| 
 | |
| 	// Create copy request with both source and destination SSE-C headers | |
| 	req := CreateTestHTTPRequest("PUT", "/dest-bucket/dest-object", nil) | |
| 
 | |
| 	// Set copy source headers | |
| 	SetupTestSSECCopyHeaders(req, sourceKey) | |
| 
 | |
| 	// Set destination headers | |
| 	SetupTestSSECHeaders(req, destKey) | |
| 
 | |
| 	// Set copy source | |
| 	req.Header.Set("X-Amz-Copy-Source", "/source-bucket/source-object") | |
| 
 | |
| 	SetupTestMuxVars(req, map[string]string{ | |
| 		"bucket": "dest-bucket", | |
| 		"object": "dest-object", | |
| 	}) | |
| 
 | |
| 	// Parse copy source headers | |
| 	copySourceKey, err := ParseSSECCopySourceHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to parse copy source headers: %v", err) | |
| 	} | |
| 
 | |
| 	if copySourceKey == nil { | |
| 		t.Fatal("Expected copy source key, got nil") | |
| 	} | |
| 
 | |
| 	if !bytes.Equal(copySourceKey.Key, sourceKey.Key) { | |
| 		t.Error("Copy source key doesn't match") | |
| 	} | |
| 
 | |
| 	// Parse destination headers | |
| 	destCustomerKey, err := ParseSSECHeaders(req) | |
| 	if err != nil { | |
| 		t.Fatalf("Failed to parse destination headers: %v", err) | |
| 	} | |
| 
 | |
| 	if destCustomerKey == nil { | |
| 		t.Fatal("Expected destination key, got nil") | |
| 	} | |
| 
 | |
| 	if !bytes.Equal(destCustomerKey.Key, destKey.Key) { | |
| 		t.Error("Destination key doesn't match") | |
| 	} | |
| } | |
| 
 | |
| // TestSSERequestValidation tests comprehensive request validation | |
| func TestSSERequestValidation(t *testing.T) { | |
| 	testCases := []struct { | |
| 		name        string | |
| 		method      string | |
| 		setupFn     func(*http.Request) | |
| 		expectError bool | |
| 		errorType   string | |
| 	}{ | |
| 		{ | |
| 			name:   "Valid PUT with SSE-C", | |
| 			method: "PUT", | |
| 			setupFn: func(req *http.Request) { | |
| 				keyPair := GenerateTestSSECKey(1) | |
| 				SetupTestSSECHeaders(req, keyPair) | |
| 			}, | |
| 			expectError: false, | |
| 		}, | |
| 		{ | |
| 			name:   "Valid GET with SSE-C", | |
| 			method: "GET", | |
| 			setupFn: func(req *http.Request) { | |
| 				keyPair := GenerateTestSSECKey(1) | |
| 				SetupTestSSECHeaders(req, keyPair) | |
| 			}, | |
| 			expectError: false, | |
| 		}, | |
| 		{ | |
| 			name:   "Invalid SSE-C key format", | |
| 			method: "PUT", | |
| 			setupFn: func(req *http.Request) { | |
| 				req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256") | |
| 				req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, "invalid-key") | |
| 				req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKeyMD5, "invalid-md5") | |
| 			}, | |
| 			expectError: true, | |
| 			errorType:   "InvalidRequest", | |
| 		}, | |
| 		{ | |
| 			name:   "Missing SSE-C key MD5", | |
| 			method: "PUT", | |
| 			setupFn: func(req *http.Request) { | |
| 				keyPair := GenerateTestSSECKey(1) | |
| 				req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerAlgorithm, "AES256") | |
| 				req.Header.Set(s3_constants.AmzServerSideEncryptionCustomerKey, keyPair.KeyB64) | |
| 				// Missing MD5 | |
| 			}, | |
| 			expectError: true, | |
| 			errorType:   "InvalidRequest", | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			req := CreateTestHTTPRequest(tc.method, "/test-bucket/test-object", []byte("test data")) | |
| 			tc.setupFn(req) | |
| 
 | |
| 			SetupTestMuxVars(req, map[string]string{ | |
| 				"bucket": "test-bucket", | |
| 				"object": "test-object", | |
| 			}) | |
| 
 | |
| 			// Test header validation | |
| 			if IsSSECRequest(req) { | |
| 				err := ValidateSSECHeaders(req) | |
| 				if tc.expectError && err == nil { | |
| 					t.Errorf("Expected error for %s, but got none", tc.name) | |
| 				} | |
| 				if !tc.expectError && err != nil { | |
| 					t.Errorf("Expected no error for %s, but got: %v", tc.name, err) | |
| 				} | |
| 			} | |
| 		}) | |
| 	} | |
| }
 |