Skip to content

Commit 1276c33

Browse files
authored
chore: add php servers
1 parent 1e69cb0 commit 1276c33

20 files changed

Lines changed: 1382 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ jobs:
2626
with:
2727
python-version: ${{ inputs.python-version || '3.11' }}
2828

29+
- name: Set up PHP with Composer
30+
uses: shivammathur/setup-php@v2
31+
with:
32+
php-version: '8.4'
33+
tools: composer:v2
34+
35+
- name: Install PHP V2 dependencies
36+
working-directory: ./test-server/php-v2-server
37+
shell: bash
38+
run: composer install
39+
40+
- name: Install PHP V3 dependencies
41+
working-directory: ./test-server/php-v3-server
42+
shell: bash
43+
run: composer install
44+
2945
# Cache uv dependencies
3046
- name: Cache uv dependencies
3147
uses: actions/cache@v3

test-server/java-tests/src/it/java/software/amazon/encryption/s3/RoundTripTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,23 @@ public class RoundTripTests {
6868
serverList.add(new LanguageServerTarget("Java-V3", "8080"));
6969
serverList.add(new LanguageServerTarget("Python-V3", "8081"));
7070
serverList.add(new LanguageServerTarget("Go-V3", "8082"));
71+
serverList.add(new LanguageServerTarget("PHP-V2", "8087"));
72+
serverList.add(new LanguageServerTarget("PHP-V3", "8093"));
7173

7274
serverMap = new HashMap<>(14);
7375
serverMap.put("Java-V3", new LanguageServerTarget("Java-V3", "8080"));
7476
serverMap.put("Python-V3", new LanguageServerTarget("Python-V3", "8081"));
7577
serverMap.put("Go-V3", new LanguageServerTarget("Go-V3", "8082"));
78+
serverMap.put("PHP-V2", new LanguageServerTarget("PHP-V2", "8087"));
79+
serverMap.put("PHP-V3", new LanguageServerTarget("PHP-V3", "8093"));
7680
}
7781

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

8589
static public class LanguageServerTarget {
8690
public String getLanguageName() {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/*
2+
cookies.txt
3+
server.pid
4+
composer.lock

test-server/php-v2-server/Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Makefile for S3 Encryption Client Testing
2+
3+
.PHONY: start-server stop-server wait-for-server
4+
5+
PID_FILE := server.pid
6+
PORT := 8087
7+
8+
start-server:
9+
@echo "Starting PHP V2 server..."
10+
AWS_ACCESS_KEY_ID="$$AWS_ACCESS_KEY_ID" \
11+
AWS_SECRET_ACCESS_KEY="$$AWS_SECRET_ACCESS_KEY" \
12+
AWS_SESSION_TOKEN="$$AWS_SESSION_TOKEN" \
13+
AWS_REGION="us-west-2" \
14+
composer run start & echo $$! > $(PID_FILE)
15+
@echo "PHP V2 server starting..."
16+
17+
stop-server:
18+
@if [ -f $(PID_FILE) ]; then \
19+
kill $$(cat $(PID_FILE)) 2>/dev/null || true; \
20+
rm $(PID_FILE); \
21+
fi
22+
23+
wait-for-server:
24+
$(MAKE) -C .. wait-for-port PORT=$(PORT)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# S3EC PHP v2 Test Server
2+
3+
This is the PHP V2 implementation of the S3ECTestServer framework. It provides a server implementation for testing S3 Encryption Client functionality.
4+
5+
## Overview
6+
7+
The S3ECPhpV2TestServer implements the S3ECTestServer service defined in the shared Smithy model. It provides endpoints for:
8+
9+
- Creating S3 Encryption Clients with session-based caching
10+
- Putting objects with encryption
11+
- Getting and decrypting objects
12+
13+
## Starting the Server
14+
15+
### Method 1: Using Composer (Recommended)
16+
```bash
17+
composer run start
18+
```
19+
20+
The server will start on port `8087`.
21+
22+
## Available Endpoints
23+
24+
### Server Status
25+
- **GET /** - Returns server status and available endpoints
26+
27+
### Client Management
28+
- **POST /client** - Creates an S3EncryptionClient and caches it with session persistence
29+
- **GET /cache** - Shows current session state and cached clients (for debugging)
30+
31+
### Object Operations
32+
- **GET /object/{bucket}/{key}** - Handle GET requests using the S3EncryptionClient
33+
- **PUT /object/{bucket}/{key}** - Handle PUT requests using the S3EncryptionClient
34+
35+
## Testing with curl
36+
37+
### Important: Session Cookie Management
38+
39+
To properly test the server and maintain session persistence, you **must** use cookies with curl:
40+
41+
#### First Request (creates session cookie):
42+
```bash
43+
curl -X POST http://localhost:8087/client \
44+
-H "Content-Type: application/json" \
45+
-c cookies.txt \
46+
-v
47+
```
48+
49+
#### Subsequent Requests (reuses session cookie):
50+
```bash
51+
curl -X POST http://localhost:8087/client \
52+
-H "Content-Type: application/json" \
53+
-b cookies.txt \
54+
-c cookies.txt \
55+
-v
56+
```
57+
58+
#### Check Cache Status:
59+
```bash
60+
curl http://localhost:8087/cache \
61+
-b cookies.txt
62+
```
63+
64+
#### Helpful Notes
65+
- **Session Storage**: Client configurations are stored in `$_SESSION['s3ecCache']`
66+
- **Object Recreation**: AWS SDK objects are recreated from stored configuration (they cannot be serialized)
67+
AWS SDK obbjects cannot be serialized due to internal resources and closures.
68+
- **Helper Function**: `getCachedClient($clientId)` retrieves and recreates clients from cache
69+
- **Debugging**: Enhanced logging and `/cache` endpoint for troubleshooting
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "aws/s3ec-php-v2-test-server",
3+
"description": "PHP v2 implementation of the S3EC Test Server framework",
4+
"type": "project",
5+
"license": "Apache-2.0",
6+
"require": {
7+
"php": ">=7.4",
8+
"aws/aws-sdk-php": "^3.356",
9+
"ramsey/uuid": "^4.9"
10+
},
11+
"autoload": {
12+
"psr-4": {
13+
"S3EC\\PhpV2Server\\": "src/"
14+
}
15+
},
16+
"scripts": {
17+
"start": [
18+
"php -S 0.0.0.0:8087 src/index.php"
19+
]
20+
},
21+
"config": {
22+
"optimize-autoloader": true
23+
}
24+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
require_once __DIR__ . '/errors.php';
4+
5+
use Ramsey\Uuid\Uuid;
6+
7+
function handleCreateClient()
8+
{
9+
// Get the raw request body
10+
$rawBody = file_get_contents('php://input');
11+
12+
// Parse JSON if the body contains JSON
13+
$requestData = json_decode($rawBody, true);
14+
if (json_last_error() !== JSON_ERROR_NONE) {
15+
return GenericServerError("Invalid JSON in request body", 400);
16+
}
17+
$configData = $requestData['config'] ?? [];
18+
$keyMaterial = $configData["keyMaterial"] ?? null;
19+
$legacyAlgorithms = $configData["enableLegacyWrappingAlgorithms"] ?? false;
20+
$clientId = Uuid::uuid4()->toString();
21+
$kmsKeyId = $keyMaterial["kmsKeyId"] ?? null;
22+
23+
if ($configData == []) {
24+
return GenericServerError("Invalid config in request body", 400);
25+
}
26+
if (($keyMaterial || $kmsKeyId) === null) {
27+
return GenericServerError("Invalid keyMaterial in config", 400);
28+
}
29+
30+
// Store client configuration instead of objects (AWS objects can't be serialized)
31+
$_SESSION['s3ecCache'][$clientId] = [
32+
's3Config' => [
33+
'region' => 'us-west-2',
34+
'version' => 'latest',
35+
'http' => [
36+
'debug' => false,
37+
'verify' => true,
38+
'curl' => [
39+
CURLOPT_VERBOSE => false,
40+
CURLOPT_NOPROGRESS => true
41+
]
42+
]
43+
],
44+
'kmsConfig' => [
45+
'region' => 'us-west-2',
46+
'version' => 'latest',
47+
'http' => [
48+
'debug' => false,
49+
'verify' => true,
50+
'curl' => [
51+
CURLOPT_VERBOSE => false,
52+
CURLOPT_NOPROGRESS => true
53+
]
54+
]
55+
],
56+
'kmsKeyId' => $kmsKeyId,
57+
'legacy' => $legacyAlgorithms,
58+
'created' => time()
59+
];
60+
61+
// Auto-update cookies.txt with current session ID so tests can access cached clients
62+
writeSessionIdToCookiesFile(session_id());
63+
64+
header("Content-Type: application/json");
65+
return json_encode([
66+
'clientId' => $clientId,
67+
]);
68+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/**
4+
* Used for "internal" errors, e.g. problems with the test server itself
5+
* Tests MUST NOT expect this error in negative tests.
6+
*
7+
* @param string $message The error message to include in the response
8+
* @param int $code The error code to set in the reponse
9+
* @return string JSON-encoded error response
10+
*/
11+
function GenericServerError($message, $code = 500)
12+
{
13+
http_response_code(500);
14+
header('Content-Type: application/json');
15+
16+
$errorResponse = [
17+
'error' => 'GenericServerError',
18+
'message' => $message
19+
];
20+
21+
return json_encode($errorResponse);
22+
}
23+
24+
/**
25+
* Used for modeled errors, e.g. errors thrown by the S3EC
26+
* Tests SHOULD expect this error in negative tests.
27+
*
28+
* @param string $message The error message to include in the response
29+
* @return string JSON-encoded error response
30+
*/
31+
function S3EncryptionClientError($message)
32+
{
33+
http_response_code(500);
34+
header('Content-Type: application/json');
35+
36+
$errorResponse = [
37+
"__type" => "software.amazon.encryption.s3#S3EncryptionClientError",
38+
'message' => $message
39+
];
40+
41+
return json_encode($errorResponse);
42+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
require_once __DIR__ . '/errors.php';
4+
5+
function handleGetObject($params)
6+
{
7+
// Get ClientID from HTTP header
8+
$clientId = $_SERVER['HTTP_X_CLIENT_ID'] ?? $_SERVER['HTTP_CLIENTID'] ?? null;
9+
10+
if (empty($clientId)) {
11+
return GenericServerError("ClientID header is required", 400);
12+
}
13+
14+
# Get the S3EncryptionClient from the client_cache
15+
$s3ecClientTuple = getCachedClient($clientId);
16+
if ($s3ecClientTuple == null) {
17+
return GenericServerError("No client found for ClientID: " . $clientId, 404);
18+
}
19+
20+
$metadata = $_SERVER['HTTP_CONTENT_METADATA'] ?? '';
21+
$encryptionContext = metadataStringToMap($metadata);
22+
23+
// Extract bucket and key from URL parameters
24+
$bucket = $params['bucket'] ?? null;
25+
$key = $params['key'] ?? null;
26+
27+
if (is_null($bucket) || is_null($key)) {
28+
return GenericServerError("Invalidb bucket or key parameters", 400);
29+
}
30+
31+
$s3ec = $s3ecClientTuple["encryptionClient"];
32+
$materialProvider = $s3ecClientTuple["materialsProvider"];
33+
$clientConfig = $s3ecClientTuple["config"];
34+
$legacyConfig = $clientConfig["legacy"] ?? false;
35+
$legacy = null;
36+
if ($legacyConfig === false) {
37+
$legacy = "V2";
38+
} else {
39+
$legacy = "V2_AND_LEGACY";
40+
}
41+
42+
try {
43+
// Start output buffering before the AWS call to capture any unwanted output
44+
ob_start();
45+
46+
$result = $s3ec->getObject([
47+
'@SecurityProfile' => $legacy,
48+
'@MaterialsProvider' => $materialProvider,
49+
'@KmsEncryptionContext' => $encryptionContext,
50+
'Bucket' => $bucket,
51+
'Key' => $key,
52+
]);
53+
54+
// Capture and discard any unwanted output from AWS SDK
55+
$unwantedOutput = ob_get_clean();
56+
if (!empty($unwantedOutput)) {
57+
error_log("AWS SDK produced unexpected output: " . strlen($unwantedOutput) . " bytes");
58+
}
59+
60+
$body = $result['Body']->getContents();
61+
$formattedMetadata = formatMetadataForResponse($result["Metadata"]);
62+
63+
// Now set headers safely
64+
header("Content-Metadata: " . $formattedMetadata);
65+
header("Content-Type: application/octet-stream");
66+
header("Content-Length: " . strlen($body));
67+
return $body;
68+
} catch (InvalidArgumentException $e) {
69+
// Clean up output buffer if still active
70+
if (ob_get_level()) {
71+
ob_end_clean();
72+
}
73+
return GenericServerError("Invalid argument: " . $e->getMessage(), 400);
74+
} catch (Exception $e) {
75+
// Clean up output buffer if still active
76+
if (ob_get_level()) {
77+
ob_end_clean();
78+
}
79+
if (strpos($e->getMessage(), "@SecurityProfile=V2") !== false) {
80+
return S3EncryptionClientError($e->getMessage() . " " . "Enable legacy wrapping algorithms to use legacy key wrapping algorithm: kms");
81+
} else {
82+
return GenericServerError("Server argument: " . $e->getMessage(), 500);
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)