Skip to content

Commit 8a70d8a

Browse files
committed
- init repository
1 parent 0929658 commit 8a70d8a

File tree

8 files changed

+482
-0
lines changed

8 files changed

+482
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Dependency directories (remove the comment below to include it)
2+
# vendor/
3+
.idea
4+
vendor

composer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "spiral/roadrunner-http",
3+
"type": "server",
4+
"description": "RoadRunner: HTTP and PSR-7 worker",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Anton Titov / Wolfy-J",
9+
"email": "[email protected]"
10+
},
11+
{
12+
"name": "RoadRunner Community",
13+
"homepage": "https://github.com/spiral/roadrunner/graphs/contributors"
14+
}
15+
],
16+
"minimum-stability": "beta",
17+
"require": {
18+
"php": ">=7.4",
19+
"spiral/roadrunner": ">=2.0",
20+
"psr/http-factory": "^1.0.1",
21+
"psr/http-message": "^1.0.1"
22+
},
23+
"require-dev": {
24+
"nyholm/psr7": "^1.3",
25+
"phpstan/phpstan": "~0.12",
26+
"phpunit/phpunit": "~8.0"
27+
},
28+
"scripts": {
29+
"analyze": "phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi"
30+
},
31+
"autoload": {
32+
"psr-4": {
33+
"Spiral\\RoadRunner\\Http\\": "src/"
34+
}
35+
}
36+
}

src/HttpWorker.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/**
4+
* High-performance PHP process supervisor and load balancer written in Go. Http core.
5+
*
6+
* @author Alex Bond
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Spiral\RoadRunner\Http;
12+
13+
use Spiral\RoadRunner\WorkerInterface;
14+
15+
class HttpWorker implements HttpWorkerInterface
16+
{
17+
/** @var WorkerInterface */
18+
private WorkerInterface $worker;
19+
20+
/**
21+
* @param WorkerInterface $worker
22+
*/
23+
public function __construct(WorkerInterface $worker)
24+
{
25+
$this->worker = $worker;
26+
}
27+
28+
/**
29+
* @return WorkerInterface
30+
*/
31+
public function getWorker(): WorkerInterface
32+
{
33+
return $this->worker;
34+
}
35+
36+
/**
37+
* Wait for incoming http request.
38+
*
39+
* @return Request|null
40+
*/
41+
public function waitRequest(): ?Request
42+
{
43+
$payload = $this->getWorker()->waitPayload();
44+
if (empty($payload->body) && empty($payload->header)) {
45+
// termination request
46+
return null;
47+
}
48+
49+
$request = new Request();
50+
$request->body = $payload->body;
51+
52+
$context = json_decode($payload->header, true);
53+
if ($context === null) {
54+
// invalid context
55+
return null;
56+
}
57+
58+
$this->hydrateRequest($request, $context);
59+
60+
return $request;
61+
}
62+
63+
/**
64+
* Send response to the application server.
65+
*
66+
* @param int $status Http status code
67+
* @param string $body Body of response
68+
* @param string[][] $headers An associative array of the message's headers. Each
69+
* key MUST be a header name, and each value MUST be an array of strings
70+
* for that header.
71+
*/
72+
public function respond(int $status, string $body, array $headers = []): void
73+
{
74+
if ($headers === []) {
75+
// this is required to represent empty header set as map and not as array
76+
$headers = new \stdClass();
77+
}
78+
79+
$this->getWorker()->send(
80+
$body,
81+
(string) json_encode(['status' => $status, 'headers' => $headers])
82+
);
83+
}
84+
85+
/**
86+
* @param Request $request
87+
* @param array $context
88+
*/
89+
private function hydrateRequest(Request $request, array $context): void
90+
{
91+
$request->remoteAddr = $context['remoteAddr'];
92+
$request->protocol = $context['protocol'];
93+
$request->method = $context['method'];
94+
$request->uri = $context['uri'];
95+
$request->attributes = $context['attributes'] ?? [];
96+
$request->headers = $context['headers'];
97+
$request->cookies = $context['cookies'] ?? [];
98+
$request->uploads = $context['uploads'] ?? [];
99+
100+
$request->query = [];
101+
parse_str($context['rawQuery'], $request->query);
102+
103+
// indicates that body was parsed
104+
$request->parsed = $context['parsed'];
105+
}
106+
}

src/HttpWorkerInterface.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/**
4+
* High-performance PHP process supervisor and load balancer written in Go. Http core.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Spiral\RoadRunner\Http;
10+
11+
interface HttpWorkerInterface extends WorkerAwareInterface
12+
{
13+
/**
14+
* Wait for incoming http request.
15+
*
16+
* @return Request|null
17+
*/
18+
public function waitRequest(): ?Request;
19+
20+
/**
21+
* Send response to the application server.
22+
*
23+
* @param int $status Http status code
24+
* @param string $body Body of response
25+
* @param string[][] $headers An associative array of the message's headers. Each
26+
* key MUST be a header name, and each value MUST be an array of strings
27+
* for that header.
28+
*/
29+
public function respond(int $status, string $body, array $headers = []): void;
30+
}

src/PSR7Worker.php

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
/**
4+
* High-performance PHP process supervisor and load balancer written in Go. Http core.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Spiral\RoadRunner\Http;
10+
11+
use Psr\Http\Message\ResponseInterface;
12+
use Psr\Http\Message\ServerRequestFactoryInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Http\Message\StreamFactoryInterface;
15+
use Psr\Http\Message\UploadedFileFactoryInterface;
16+
use Psr\Http\Message\UploadedFileInterface;
17+
use Spiral\RoadRunner\WorkerInterface;
18+
19+
/**
20+
* Manages PSR-7 request and response.
21+
*/
22+
class PSR7Worker
23+
{
24+
private HttpWorker $httpWorker;
25+
private ServerRequestFactoryInterface $requestFactory;
26+
private StreamFactoryInterface $streamFactory;
27+
private UploadedFileFactoryInterface $uploadsFactory;
28+
29+
/** @var mixed[] */
30+
private array $originalServer = [];
31+
32+
/** @var string[] Valid values for HTTP protocol version */
33+
private static array $allowedVersions = ['1.0', '1.1', '2',];
34+
35+
/**
36+
* @param WorkerInterface $worker
37+
* @param ServerRequestFactoryInterface $requestFactory
38+
* @param StreamFactoryInterface $streamFactory
39+
* @param UploadedFileFactoryInterface $uploadsFactory
40+
*/
41+
public function __construct(
42+
WorkerInterface $worker,
43+
ServerRequestFactoryInterface $requestFactory,
44+
StreamFactoryInterface $streamFactory,
45+
UploadedFileFactoryInterface $uploadsFactory
46+
) {
47+
$this->httpWorker = new HttpWorker($worker);
48+
$this->requestFactory = $requestFactory;
49+
$this->streamFactory = $streamFactory;
50+
$this->uploadsFactory = $uploadsFactory;
51+
$this->originalServer = $_SERVER;
52+
}
53+
54+
/**
55+
* @return WorkerInterface
56+
*/
57+
public function getWorker(): WorkerInterface
58+
{
59+
return $this->httpWorker->getWorker();
60+
}
61+
62+
/**
63+
* @return ServerRequestInterface|null
64+
*/
65+
public function waitRequest(): ?ServerRequestInterface
66+
{
67+
$httpRequest = $this->httpWorker->waitRequest();
68+
if ($httpRequest === null) {
69+
return null;
70+
}
71+
72+
$_SERVER = $this->configureServer($httpRequest);
73+
74+
return $this->mapRequest($httpRequest, $_SERVER);
75+
}
76+
77+
/**
78+
* Send response to the application server.
79+
*
80+
* @param ResponseInterface $response
81+
*/
82+
public function respond(ResponseInterface $response): void
83+
{
84+
$this->httpWorker->respond(
85+
$response->getStatusCode(),
86+
$response->getBody()->__toString(),
87+
$response->getHeaders()
88+
);
89+
}
90+
91+
/**
92+
* Returns altered copy of _SERVER variable. Sets ip-address,
93+
* request-time and other values.
94+
*
95+
* @param Request $request
96+
* @return mixed[]
97+
*/
98+
protected function configureServer(Request $request): array
99+
{
100+
$server = $this->originalServer;
101+
102+
$server['REQUEST_URI'] = $request->uri;
103+
$server['REQUEST_TIME'] = time();
104+
$server['REQUEST_TIME_FLOAT'] = microtime(true);
105+
$server['REMOTE_ADDR'] = $request->getRemoteAddr();
106+
$server['REQUEST_METHOD'] = $request->method;
107+
108+
$server['HTTP_USER_AGENT'] = '';
109+
foreach ($request->headers as $key => $value) {
110+
$key = strtoupper(str_replace('-', '_', $key));
111+
if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
112+
$server[$key] = implode(', ', $value);
113+
} else {
114+
$server['HTTP_' . $key] = implode(', ', $value);
115+
}
116+
}
117+
118+
return $server;
119+
}
120+
121+
/**
122+
* @param Request $httpRequest
123+
* @param array $server
124+
* @return ServerRequestInterface
125+
*/
126+
protected function mapRequest(Request $httpRequest, array $server): ServerRequestInterface
127+
{
128+
$request = $this->requestFactory->createServerRequest(
129+
$httpRequest->method,
130+
$httpRequest->uri,
131+
$_SERVER
132+
);
133+
134+
135+
$request = $request
136+
->withProtocolVersion(static::fetchProtocolVersion($httpRequest->protocol))
137+
->withCookieParams($httpRequest->cookies)
138+
->withQueryParams($httpRequest->query)
139+
->withUploadedFiles($this->wrapUploads($httpRequest->uploads));
140+
141+
foreach ($httpRequest->attributes as $name => $value) {
142+
$request = $request->withAttribute($name, $value);
143+
}
144+
145+
foreach ($httpRequest->headers as $name => $value) {
146+
$request = $request->withHeader($name, $value);
147+
}
148+
149+
if ($httpRequest->parsed) {
150+
return $request->withParsedBody($httpRequest->getParsedBody());
151+
}
152+
153+
if ($httpRequest->body !== null) {
154+
return $request->withBody($this->streamFactory->createStream($httpRequest->body));
155+
}
156+
157+
return $request;
158+
}
159+
160+
/**
161+
* Wraps all uploaded files with UploadedFile.
162+
*
163+
* @param array[] $files
164+
* @return UploadedFileInterface[]|mixed[]
165+
*/
166+
protected function wrapUploads(array $files): array
167+
{
168+
$result = [];
169+
foreach ($files as $index => $f) {
170+
if (!isset($f['name'])) {
171+
$result[$index] = $this->wrapUploads($f);
172+
continue;
173+
}
174+
175+
if (UPLOAD_ERR_OK === $f['error']) {
176+
$stream = $this->streamFactory->createStreamFromFile($f['tmpName']);
177+
} else {
178+
$stream = $this->streamFactory->createStream();
179+
}
180+
181+
$result[$index] = $this->uploadsFactory->createUploadedFile(
182+
$stream,
183+
$f['size'],
184+
$f['error'],
185+
$f['name'],
186+
$f['mime']
187+
);
188+
}
189+
190+
return $result;
191+
}
192+
193+
/**
194+
* Normalize HTTP protocol version to valid values
195+
*
196+
* @param string $version
197+
* @return string
198+
*/
199+
private static function fetchProtocolVersion(string $version): string
200+
{
201+
$v = substr($version, 5);
202+
203+
if ($v === '2.0') {
204+
return '2';
205+
}
206+
207+
// Fallback for values outside of valid protocol versions
208+
if (!in_array($v, static::$allowedVersions, true)) {
209+
return '1.1';
210+
}
211+
212+
return $v;
213+
}
214+
}

0 commit comments

Comments
 (0)