Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0bd6c10
ref: remove deprecated ErrorHandler class
milinsky May 19, 2026
702de62
ref: remove @psalm-suppress and add assert for type safety
milinsky May 19, 2026
cf106c6
ref: remove inline comments and fix boolean style in ProductionErrorH…
milinsky May 19, 2026
6dd4b64
ref: remove @package tags from DTO classes
milinsky May 19, 2026
c382e75
ref: remove deprecated ErrorHandler tests and references
milinsky May 19, 2026
daa9a5b
ref: remove unused writeChunked and writeBuffered from ResponseWriter
milinsky May 19, 2026
8bd805f
ref: remove cleanupInvalidConnections and inline clearRequestQueue in…
milinsky May 19, 2026
75f2693
ref: remove unused getPool from ConnectionManager
milinsky May 19, 2026
54793b5
ref: fix boolean checks to Yoda style across src/
milinsky May 19, 2026
e40db2e
ref: add buffer size limit to Connection appendToBuffer
milinsky May 19, 2026
db27e2b
ref: pass maxBufferSize from ServerConfig through ConnectionManager
milinsky May 19, 2026
b7980a5
ref: add isClosed guards after appendToBuffer calls
milinsky May 19, 2026
00eab73
ref: add CorsService with preflight handling and CORS headers
milinsky May 19, 2026
57e5fca
ref: add CSP, configurable HSTS, fix X-XSS-Protection
milinsky May 19, 2026
8dd6997
ref: fix HTTP pipelining, integrate AuditLogger, add Date header
milinsky May 19, 2026
5f3097d
perf: batched socket_select, IteratorAggregate, remove array_values
milinsky May 19, 2026
05e2308
perf: chunk-based buffer, cached realpath, TCP_NODELAY, optimized Rat…
milinsky May 19, 2026
6d97e10
fix: orphan cleanup in $requestConnections, TempFileManager shutdown …
milinsky May 19, 2026
3a14689
refactor: extract RequestQueue, ResponseSender, ClientIpResolver from…
milinsky May 19, 2026
3e710ac
refactor: logger injection via constructors + callable to interface r…
milinsky May 19, 2026
52bdca2
refactor: split ServerInterface into ISP sub-interfaces
milinsky May 19, 2026
029ba1c
test: add comprehensive functional tests (HTTP, WebSocket, WorkerPool…
milinsky May 19, 2026
0ffaf7b
test: add security & performance tests, audit naming conventions
milinsky May 19, 2026
a360289
docs: add security configuration, architecture overview, and unit tes…
milinsky May 19, 2026
0ce8ea9
chore: rename ErrorHandler class
milinsky May 20, 2026
bf8554e
chore: Fix cs
milinsky May 20, 2026
84769ab
feat: add getPeerName() and exportStream() to SocketResourceInterface
milinsky May 20, 2026
be7a44c
feat: implement getPeerName() and exportStream() in StreamSocketResource
milinsky May 20, 2026
759f52e
feat: implement getPeerName() and exportStream() in SocketInterface c…
milinsky May 20, 2026
2277edc
chore: add infection for mutation testing and apply rector formatting
milinsky May 20, 2026
54f03a8
feat: add NotificationSocketPairInterface for reactive event loop sig…
milinsky May 20, 2026
f20e45e
feat: implement SocketNotificationPair with full test coverage
milinsky May 20, 2026
8693843
refactor: replace socket_getpeername() with SocketResourceInterface::…
milinsky May 20, 2026
2fd1cb0
refactor: replace socket_* calls with StreamSocketResource methods in…
milinsky May 20, 2026
df6e822
refactor: replace socket_* calls with NotificationSocketPairInterface…
milinsky May 20, 2026
baec670
refactor: merge duplicate mask/unmask methods in WebSocket Frame
milinsky May 20, 2026
3842a9a
refactor: delegate reset() to closeAll() in WebSocketHandler
milinsky May 20, 2026
d6614a4
refactor: unify keep-alive resolution logic into resolveKeepAlive() m…
milinsky May 20, 2026
c3dc57c
refactor: extract processFrameLoop() from duplicated WebSocket frame …
milinsky May 20, 2026
81bc0fb
refactor: extract readAndProcess() from duplicated connection read logic
milinsky May 20, 2026
fb106ab
refactor: extract validateAndOpenFile() from duplicated file download…
milinsky May 20, 2026
e24a548
refactor: extract configureClient() for duplicated socket setup in ac…
milinsky May 20, 2026
349d7c3
refactor: consolidate MIME type maps into shared MimeTypeMap utility
milinsky May 20, 2026
b7eb6ec
refactor: unify remote address resolution into ClientIpResolver::reso…
milinsky May 20, 2026
153d2cb
refactor: delegate close connection to ConnectionManager via interface
milinsky May 20, 2026
1ee6028
fix: add Connection: close header to rate limit integration test
milinsky May 20, 2026
a07df3b
refactor: remove setAccessible(true) from tests as no-op since PHP 8.1
milinsky May 20, 2026
6024086
refactor: create ErrorReportingScope trait to replace error_reporting…
milinsky May 20, 2026
a4ebe99
refactor: replace expectNotToPerformAssertions with meaningful assert…
milinsky May 20, 2026
26742e1
refactor: group ev/pcntl tests and enable strict phpunit flags
milinsky May 20, 2026
a3cd262
test: rewrite NotificationManager tests with mock SocketPair
milinsky May 20, 2026
407a580
test: add ConnectionManager coverage via mock SocketResourceInterface
milinsky May 20, 2026
0002ecb
test: add Server method coverage via mock SocketResourceInterface
milinsky May 20, 2026
b2bde3b
test: add WebSocket component coverage via mock connections
milinsky May 20, 2026
e46c8b5
test: add edge case and error path coverage
milinsky May 20, 2026
b423014
test: add integration tests for Socket wrappers + final metrics valid…
milinsky May 20, 2026
b0a5192
chore: cleanUp old code
milinsky May 21, 2026
1731d8d
fix: replace createMock() with createStub() for mocks without expecta…
milinsky May 21, 2026
8a4abe4
fix: suppress ErrorHandler/SSL console output in tests
milinsky May 21, 2026
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
270 changes: 254 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,48 +87,286 @@ use Duyler\HttpServer\Config\ServerConfig;

$config = new ServerConfig(
// Network
host: '0.0.0.0', // Bind address
port: 8080, // Bind port
host: '0.0.0.0', // Bind address (IP or hostname)
port: 8080, // Bind port (1-65535)
socketBacklog: 511, // TCP backlog queue size
maxAcceptsPerCycle: 10, // Max new connections per event cycle

// SSL/TLS
ssl: false, // Enable HTTPS
sslCert: null, // Path to SSL certificate
sslKey: null, // Path to SSL private key
sslCert: null, // Path to SSL certificate file
sslKey: null, // Path to SSL private key file

// Static Files
publicPath: null, // Path to public directory
publicPath: null, // Path to public directory for static serving
enableStaticCache: true, // Enable in-memory static file cache
staticCacheSize: 52428800, // Max cache size (50MB)

// Timeouts
requestTimeout: 30, // Request timeout in seconds
connectionTimeout: 60, // Connection timeout in seconds

// Limits
maxConnections: 1000, // Maximum concurrent connections
maxRequestSize: 10485760, // Max request size (10MB)
bufferSize: 8192, // Read buffer size
maxRequestSize: 10485760, // Max request body size (10MB)
bufferSize: 8192, // Read buffer size in bytes
headerCacheLimit: 100, // Max cached header strings
memoryLimit: 134217728, // Memory limit in bytes (128MB)

// Keep-Alive
enableKeepAlive: true, // Enable persistent connections
enableKeepAlive: true, // Enable HTTP persistent connections
keepAliveTimeout: 30, // Keep-alive timeout in seconds
keepAliveMaxRequests: 100, // Max requests per connection

// Static Cache
enableStaticCache: true, // Enable in-memory static file cache
staticCacheSize: 52428800, // Max cache size (50MB)
keepAliveMaxRequests: 100, // Max requests per keep-alive connection

// Rate Limiting
enableRateLimit: false, // Enable rate limiting
enableRateLimit: false, // Enable rate limiting per IP
rateLimitRequests: 100, // Max requests per window
rateLimitWindow: 60, // Rate limit window in seconds

// Performance
maxAcceptsPerCycle: 10, // Max new connections per cycle
// CORS
enableCors: false, // Enable CORS handling
corsAllowedOrigins: [], // Allowed origins (required when enabled)
corsAllowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
corsAllowedHeaders: ['Content-Type', 'Authorization'],
corsAllowCredentials: false, // Allow credentials (no wildcard origin)
corsMaxAge: 86400, // Preflight cache duration
corsExposeHeaders: [], // Headers exposed to client

// CSP (Content Security Policy)
contentSecurityPolicy: null, // CSP directives as array
contentSecurityPolicyReportOnly: null, // Report-only CSP directives
enableCspNonce: false, // Generate per-request CSP nonce

// HSTS (HTTP Strict Transport Security)
enableHsts: false, // Enable HSTS header (requires SSL)
hstsMaxAge: 31536000, // HSTS max-age in seconds
hstsIncludeSubDomains: false, // Include subdomains in HSTS
hstsPreload: false, // Opt-in to HSTS preload lists

// Security Headers
enableSecurityHeaders: true, // Enable automatic security headers
frameOptions: 'DENY', // X-Frame-Options: DENY or SAMEORIGIN
referrerPolicy: 'strict-origin-when-cross-origin', // Referrer-Policy value
permissionsPolicy: 'geolocation=(), microphone=(), camera=()', // Permissions-Policy

// Debug
debugMode: false, // Enable debug logging mode
);
```

## Security Configuration

### CORS (Cross-Origin Resource Sharing)

```php
$config = new ServerConfig(
enableCors: true,
corsAllowedOrigins: ['https://example.com', 'https://app.example.com'],
corsAllowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
corsAllowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
corsAllowCredentials: false,
corsMaxAge: 86400,
corsExposeHeaders: ['X-Custom-Header'],
);
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enableCors` | `bool` | `false` | Enable CORS handling |
| `corsAllowedOrigins` | `list<string>` | `[]` | Allowed origin URLs (required when enabled) |
| `corsAllowedMethods` | `list<string>` | `['GET','POST','PUT','DELETE','OPTIONS']` | Allowed HTTP methods |
| `corsAllowedHeaders` | `list<string>` | `['Content-Type','Authorization']` | Allowed request headers |
| `corsAllowCredentials` | `bool` | `false` | Allow cookies/auth headers (incompatible with wildcard origin) |
| `corsMaxAge` | `int` | `86400` | Preflight cache duration in seconds |
| `corsExposeHeaders` | `list<string>` | `[]` | Headers exposed to JavaScript |

### Content Security Policy (CSP)

```php
$config = new ServerConfig(
contentSecurityPolicy: [
'default-src' => ["'self'"],
'script-src' => ["'self'", "'nonce-{{NONCE}}'"],
'style-src' => ["'self'", "'unsafe-inline'"],
'img-src' => ["'self'", 'data:', 'https:'],
'connect-src' => ["'self'", 'wss://example.com'],
],
enableCspNonce: true,
);
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `contentSecurityPolicy` | `?array` | `null` | CSP directives as key-value map |
| `contentSecurityPolicyReportOnly` | `?array` | `null` | Report-only CSP directives |
| `enableCspNonce` | `bool` | `false` | Generate per-request CSP nonce (placeholder: `{{NONCE}}`) |

### HTTP Strict Transport Security (HSTS)

```php
$config = new ServerConfig(
ssl: true,
enableHsts: true,
hstsMaxAge: 31536000,
hstsIncludeSubDomains: true,
hstsPreload: false,
);
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enableHsts` | `bool` | `false` | Enable HSTS header |
| `hstsMaxAge` | `int` | `31536000` | Max-age in seconds (1 year default) |
| `hstsIncludeSubDomains` | `bool` | `false` | Include all subdomains |
| `hstsPreload` | `bool` | `false` | Opt-in to browser HSTS preload lists |

### Permissions Policy

```php
$config = new ServerConfig(
permissionsPolicy: 'geolocation=(), microphone=(), camera=(), payment=(self)',
);
```

### Other Security Headers

The server automatically adds these headers when `enableSecurityHeaders` is true (default):

| Header | Default Value |
|--------|---------------|
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `DENY` (configurable: `DENY` or `SAMEORIGIN`) |
| `X-XSS-Protection` | `1; mode=block` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` |

```php
$config = new ServerConfig(
enableSecurityHeaders: true,
frameOptions: 'SAMEORIGIN',
referrerPolicy: 'strict-origin-when-cross-origin',
);
```

## Architecture Overview

### Request Processing Pipeline

```
Client Request
|
v
+-------------+ +------------------+ +----------------+
| Server |---->| ConnectionPool |---->| HttpParser |
| (accept) | | (manage conns) | | (parse HTTP) |
+-------------+ +------------------+ +----------------+
|
v
+------------------+ +----------------+
| CorsService |---->| RateLimiter |
| (CORS check) | | (throttle) |
+------------------+ +----------------+
|
v
+------------------+ +----------------+
| SecurityHeaders |---->| AuditLogger |
| (add headers) | | (log request) |
+------------------+ +----------------+
|
v
+------------------+ +----------------+
| RequestQueue |---->| ResponseSender |
| (enqueue) | | (send) |
+------------------+ +----------------+
|
v
Client Response
```

### Component Responsibilities

| Component | Responsibility |
|-----------|---------------|
| `Server` | Entry point. Accepts connections, delegates to processor |
| `HttpRequestProcessor` | Orchestrates request lifecycle: read, parse, security check, enqueue |
| `ConnectionPool` | Manages connection lifecycle, enforces max connections |
| `RequestQueue` | Thread-safe request queue with ID-based response mapping |
| `ResponseSender` | Writes HTTP responses back to client connections |
| `ClientIpResolver` | Resolves real client IP from X-Forwarded-For chain |
| `CorsService` | Validates CORS requests and adds response headers |
| `SecurityHeadersService` | Adds CSP, HSTS, X-Frame-Options, Permissions-Policy |
| `RateLimiter` | Sliding window rate limiting per client IP |
| `AuditLogger` | PSR-3 audit logging for security events |

### ServerInterface Decomposition

```
ServerInterface
|
+---------------+---------------+
| | |
RequestLifecycle ServerLifecycle Metrics
Interface Interface Interface
|
WorkerPoolIntegration
Interface
```

- **RequestLifecycleInterface** -- `hasRequest()`, `getRequest()`, `respond()`, `hasPendingResponse()`
- **ServerLifecycleInterface** -- `start()`, `stop()`, `reset()`, `restart()`, `shutdown()`
- **WorkerPoolIntegrationInterface** -- `setWorkerId()`, `addExternalConnection()`, `enableNotification()`, `getSocketResource()`
- **MetricsInterface** -- `getMetrics()`

### WebSocket Pipeline

```
HTTP Upgrade Request
|
v
+------------------+
| Handshake | (validate Origin, compute accept key)
+------------------+
|
v
+------------------+
| WebSocketHandler | (frame parsing, message dispatch)
+------------------+
|
v
+------------------+
| WebSocketServer | (connection management, broadcast)
+------------------+
|
+----+----+
| |
v v
onMessage onClose
| |
v v
Connection Cleanup
```

### Worker Pool Integration

```
+-------------------+
| Worker Pool |
| Master Process |
+-------------------+
|
+----+----+----+
| | |
v v v
+--------+ +--------+ +--------+
|Worker 1| |Worker 2| |Worker N|
| Server | | Server | | Server |
+--------+ +--------+ +--------+
| | |
+----Shared Socket Pool----+
|
v
Client Requests
```

## Advanced Usage

### HTTPS Server
Expand Down
11 changes: 8 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"php": "^8.4",
"ext-ev": "*",
"ext-sockets": "*",
"ext-pcntl": "*",
"nyholm/psr7": "^1.8",
"nyholm/psr7-server": "^1.1",
"psr/http-factory": "^1.1",
Expand All @@ -31,9 +32,10 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.90",
"vimeo/psalm": "^6.10",
"infection/infection": "*",
"phpunit/phpunit": "^13.0",
"rector/rector": "^1.2"
"rector/rector": "^1.2",
"vimeo/psalm": "^6.10"
},
"autoload": {
"psr-4": {
Expand All @@ -47,7 +49,10 @@
},
"config": {
"sort-packages": true,
"optimize-autoloader": true
"optimize-autoloader": true,
"allow-plugins": {
"infection/extension-installer": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
Expand Down
11 changes: 11 additions & 0 deletions infection.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "vendor/infection/infection/resources/schema.json",
"source": {
"directories": [
"src"
]
},
"mutators": {
"@default": true
}
}
8 changes: 7 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/13.0/phpunit.xsd" bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache" executionOrder="depends,defects" requireCoverageMetadata="false" beStrictAboutCoverageMetadata="false" beStrictAboutOutputDuringTests="false" failOnRisky="false" failOnWarning="false" colors="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" displayDetailsOnTestsThatTriggerDeprecations="true" displayDetailsOnTestsThatTriggerErrors="true" displayDetailsOnTestsThatTriggerNotices="true" displayDetailsOnTestsThatTriggerWarnings="true" displayDetailsOnPhpunitDeprecations="true" stopOnSkipped="false">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/13.0/phpunit.xsd" bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache" executionOrder="depends,defects" requireCoverageMetadata="false" beStrictAboutCoverageMetadata="false" beStrictAboutOutputDuringTests="false" failOnRisky="true" failOnWarning="true" failOnNotice="true" colors="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" displayDetailsOnTestsThatTriggerDeprecations="true" displayDetailsOnTestsThatTriggerErrors="true" displayDetailsOnTestsThatTriggerNotices="true" displayDetailsOnTestsThatTriggerWarnings="true" displayDetailsOnPhpunitDeprecations="true" stopOnSkipped="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>ev</group>
<group>pcntl</group>
</exclude>
</groups>
<source restrictNotices="true" restrictWarnings="true" ignoreIndirectDeprecations="true">
<include>
<directory>src</directory>
Expand Down
Loading
Loading