Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ jobs:
with:
python-version: ${{ inputs.python-version || '3.11' }}

- name: Set up PHP with Composer
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer:v2

- name: Install PHP V2 dependencies
working-directory: ./test-server/php-v2-server
shell: bash
run: composer install

- name: Install PHP V3 dependencies
working-directory: ./test-server/php-v3-server
shell: bash
run: composer install

# Cache uv dependencies
- name: Cache uv dependencies
uses: actions/cache@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,23 @@ public class RoundTripTests {
serverList.add(new LanguageServerTarget("Java-V3", "8080"));
serverList.add(new LanguageServerTarget("Python-V3", "8081"));
serverList.add(new LanguageServerTarget("Go-V3", "8082"));
serverList.add(new LanguageServerTarget("PHP-V2", "8087"));
serverList.add(new LanguageServerTarget("PHP-V3", "8093"));

serverMap = new HashMap<>(14);
serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080"));
serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081"));
serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082"));
serverMap.put("PHP-V2", new LanguageServerTarget("PHP-V2", "8087"));
serverMap.put("PHP-V3", new LanguageServerTarget("PHP-V3", "8093"));
}

// These S3EC implementations do not validate encryption context provided to getObject (i.e. on decrypt).
// If the encryption context provided to getObject does not match the encryption context on the stored object,
// these implementations will not raise an error as expected.
// For now, skip tests that expect encryption context validation on decrypt.
private static final Set<String> ENCRYPTION_CONTEXT_ON_DECRYPT_UNSUPPORTED =
Set.of("Go-V3");
Set.of("Go-V3", "PHP-V2", "PHP-V3");

static public class LanguageServerTarget {
public String getLanguageName() {
Expand Down
4 changes: 4 additions & 0 deletions test-server/php-v2-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
vendor/*
cookies.txt
server.pid
composer.lock
24 changes: 24 additions & 0 deletions test-server/php-v2-server/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Makefile for S3 Encryption Client Testing

.PHONY: start-server stop-server wait-for-server

PID_FILE := server.pid
PORT := 8087

start-server:
@echo "Starting PHP V2 server..."
AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \
AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \
AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \
AWS_REGION="us-west-2" \
composer run start & echo $$! > $(PID_FILE)
@echo "PHP V2 server starting..."

stop-server:
@if [ -f $(PID_FILE) ]; then \
kill $$(cat $(PID_FILE)) 2>/dev/null || true; \
rm $(PID_FILE); \
fi

wait-for-server:
$(MAKE) -C .. wait-for-port PORT=$(PORT)
69 changes: 69 additions & 0 deletions test-server/php-v2-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# S3EC PHP v2 Test Server

This is the PHP V2 implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality.

## Overview

The S3ECPhpV2TestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for:

- Creating S3 Encryption Clients with session-based caching
- Putting objects with encryption
- Getting and decrypting objects

## Starting the Server

### Method 1: Using Composer (Recommended)
```bash
composer run start
```

The server will start on port `8087`.

## Available Endpoints

### Server Status
- **GET /** - Returns server status and available endpoints

### Client Management
- **POST /client** - Creates an S3EncryptionClient and caches it with session persistence
- **GET /cache** - Shows current session state and cached clients (for debugging)

### Object Operations
- **GET /object/{bucket}/{key}** - Handle GET requests using the S3EncryptionClient
- **PUT /object/{bucket}/{key}** - Handle PUT requests using the S3EncryptionClient

## Testing with curl

### Important: Session Cookie Management

To properly test the server and maintain session persistence, you **must** use cookies with curl:

#### First Request (creates session cookie):
```bash
curl -X POST http://localhost:8087/client \
-H "Content-Type: application/json" \
-c cookies.txt \
-v
```

#### Subsequent Requests (reuses session cookie):
```bash
curl -X POST http://localhost:8087/client \
-H "Content-Type: application/json" \
-b cookies.txt \
-c cookies.txt \
-v
```

#### Check Cache Status:
```bash
curl http://localhost:8087/cache \
-b cookies.txt
```

#### Helpful Notes
- **Session Storage**: Client configurations are stored in `$_SESSION['s3ecCache']`
- **Object Recreation**: AWS SDK objects are recreated from stored configuration (they cannot be serialized)
AWS SDK obbjects cannot be serialized due to internal resources and closures.
- **Helper Function**: `getCachedClient($clientId)` retrieves and recreates clients from cache
- **Debugging**: Enhanced logging and `/cache` endpoint for troubleshooting
24 changes: 24 additions & 0 deletions test-server/php-v2-server/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "aws/s3ec-php-v2-test-server",
"description": "PHP v2 implementation of the S3EC Test Server framework",
"type": "project",
"license": "Apache-2.0",
"require": {
"php": ">=7.4",
"aws/aws-sdk-php": "^3.356",
"ramsey/uuid": "^4.9"
},
"autoload": {
"psr-4": {
"S3EC\\PhpV2Server\\": "src/"
}
},
"scripts": {
"start": [
"php -S 0.0.0.0:8087 src/index.php"
]
},
"config": {
"optimize-autoloader": true
}
}
68 changes: 68 additions & 0 deletions test-server/php-v2-server/src/client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

require_once __DIR__ . '/errors.php';

use Ramsey\Uuid\Uuid;

function handleCreateClient()
{
// Get the raw request body
$rawBody = file_get_contents('php://input');

// Parse JSON if the body contains JSON
$requestData = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return GenericServerError("Invalid JSON in request body", 400);
}
$configData = $requestData['config'] ?? [];
$keyMaterial = $configData["keyMaterial"] ?? null;
$legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false;
$clientId = Uuid::uuid4()->toString();
$kmsKeyId = $keyMaterial["kmsKeyId"] ?? null;

if ($configData == []) {
return GenericServerError("Invalid config in request body", 400);
}
if (($keyMaterial || $kmsKeyId) === null) {
return GenericServerError("Invalid keyMaterial in config", 400);
}

// Store client configuration instead of objects (AWS objects can't be serialized)
$_SESSION['s3ecCache'][$clientId] = [
's3Config' => [
'region' => 'us-west-2',
'version' => 'latest',
'http' => [
'debug' => false,
'verify' => true,
'curl' => [
CURLOPT_VERBOSE => false,
CURLOPT_NOPROGRESS => true
]
]
],
'kmsConfig' => [
'region' => 'us-west-2',
'version' => 'latest',
'http' => [
'debug' => false,
'verify' => true,
'curl' => [
CURLOPT_VERBOSE => false,
CURLOPT_NOPROGRESS => true
]
]
],
'kmsKeyId' => $kmsKeyId,
'legacy' => $legacyAlgorithms,
'created' => time()
];

// Auto-update cookies.txt with current session ID so tests can access cached clients
writeSessionIdToCookiesFile(session_id());

header("Content-Type: application/json");
return json_encode([
'clientId' => $clientId,
]);
}
42 changes: 42 additions & 0 deletions test-server/php-v2-server/src/errors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/**
* Used for "internal" errors, e.g. problems with the test server itself
* Tests MUST NOT expect this error in negative tests.
*
* @param string $message The error message to include in the response
* @param int $code The error code to set in the reponse
* @return string JSON-encoded error response
*/
function GenericServerError($message, $code = 500)
{
http_response_code(500);
header('Content-Type: application/json');

$errorResponse = [
'error' => 'GenericServerError',
'message' => $message
];

return json_encode($errorResponse);
}

/**
* Used for modeled errors, e.g. errors thrown by the S3EC
* Tests SHOULD expect this error in negative tests.
*
* @param string $message The error message to include in the response
* @return string JSON-encoded error response
*/
function S3EncryptionClientError($message)
{
http_response_code(500);
header('Content-Type: application/json');

$errorResponse = [
"__type" => "software.amazon.encryption.s3#S3EncryptionClientError",
'message' => $message
];

return json_encode($errorResponse);
}
85 changes: 85 additions & 0 deletions test-server/php-v2-server/src/get_object.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

require_once __DIR__ . '/errors.php';

function handleGetObject($params)
{
// Get ClientID from HTTP header
$clientId = $_SERVER['HTTP_X_CLIENT_ID'] ?? $_SERVER['HTTP_CLIENTID'] ?? null;

if (empty($clientId)) {
return GenericServerError("ClientID header is required", 400);
}

# Get the S3EncryptionClient from the client_cache
$s3ecClientTuple = getCachedClient($clientId);
if ($s3ecClientTuple == null) {
return GenericServerError("No client found for ClientID: " . $clientId, 404);
}

$metadata = $_SERVER['HTTP_CONTENT_METADATA'] ?? '';
$encryptionContext = metadataStringToMap($metadata);

// Extract bucket and key from URL parameters
$bucket = $params['bucket'] ?? null;
$key = $params['key'] ?? null;

if (is_null($bucket) || is_null($key)) {
return GenericServerError("Invalidb bucket or key parameters", 400);
}

$s3ec = $s3ecClientTuple["encryptionClient"];
$materialProvider = $s3ecClientTuple["materialsProvider"];
$clientConfig = $s3ecClientTuple["config"];
$legacyConfig = $clientConfig["legacy"] ?? false;
$legacy = null;
if ($legacyConfig === false) {
$legacy = "V2";
} else {
$legacy = "V2_AND_LEGACY";
}

try {
// Start output buffering before the AWS call to capture any unwanted output
ob_start();

$result = $s3ec->getObject([
'@SecurityProfile' => $legacy,
'@MaterialsProvider' => $materialProvider,
'@KmsEncryptionContext' => $encryptionContext,
'Bucket' => $bucket,
'Key' => $key,
]);

// Capture and discard any unwanted output from AWS SDK
$unwantedOutput = ob_get_clean();
if (!empty($unwantedOutput)) {
error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes");
}

$body = $result['Body']->getContents();
$formattedMetadata = formatMetadataForResponse($result["Metadata"]);

// Now set headers safely
header("Content-Metadata: " . $formattedMetadata);
header("Content-Type: application/octet-stream");
header("Content-Length: " . strlen($body));
return $body;
} catch (InvalidArgumentException $e) {
// Clean up output buffer if still active
if (ob_get_level()) {
ob_end_clean();
}
return GenericServerError("Invalid argument: " . $e->getMessage(), 400);
} catch (Exception $e) {
// Clean up output buffer if still active
if (ob_get_level()) {
ob_end_clean();
}
if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) {
return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms");
} else {
return GenericServerError("Server argument: " . $e->getMessage(), 500);
}
}
}
Loading