Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 15be013

Browse files
authoredApr 10, 2021
Convert to proper Symfony requests (#31)
* Adding better convert to Symfony request * Update readme * fix laravel
1 parent ba7176e commit 15be013

File tree

7 files changed

+300
-69
lines changed

7 files changed

+300
-69
lines changed
 

‎README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Define the environment variable `APP_RUNTIME` for your application on Lambda.
2929

3030
## How to use
3131

32-
You need the extra lambda layer `arn:aws:lambda:eu-central-1:403367587399:layer:bref-symfony-runtime:3`
32+
You need the extra lambda layer `arn:aws:lambda:[region]:403367587399:layer:bref-sf-runtime:1`
3333
in serverless.yml.
3434

3535
```yaml
@@ -43,7 +43,7 @@ functions:
4343
timeout: 8
4444
layers:
4545
- ${bref:layer.php-74}
46-
- arn:aws:lambda:eu-central-1:403367587399:layer:bref-symfony-runtime:3
46+
- arn:aws:lambda:eu-central-1:403367587399:layer:bref-sf-runtime:1
4747
events:
4848
- httpApi: '*'
4949
```

‎composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"bref/bref": "^1.2",
1414
"clue/arguments": "^2.1",
1515
"psr/http-server-handler": "^1.0",
16+
"riverline/multipart-parser": "^2.0",
1617
"symfony/runtime": "5.x-dev"
1718
},
1819
"require-dev": {

‎src/LaravelHttpHandler.php

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,42 +25,11 @@ public function __construct(Kernel $kernel)
2525

2626
public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse
2727
{
28-
Request::setTrustedProxies(['127.0.0.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO);
29-
30-
// CGI Version 1.1 - Section 4.1
31-
$server = array_filter([
32-
'AUTH_TYPE' => $event->getHeaders()['auth-type'] ?? null, // 4.1.1
33-
'CONTENT_LENGTH' => $event->getHeaders()['content-length'] ?? null, // 4.1.2
34-
'CONTENT_TYPE' => $event->getContentType(), // 4.1.3
35-
'QUERY_STRING' => $event->getQueryString(), // 4.1.7
36-
'REQUEST_METHOD' => $event->getMethod(), // 4.1.12
37-
'SERVER_PORT' => $event->getServerPort(), // 4.1.16
38-
'SERVER_PROTOCOL' => $event->getProtocolVersion(), // 4.1.16
39-
'DOCUMENT_ROOT' => getcwd(),
40-
'REQUEST_TIME' => time(),
41-
'REQUEST_TIME_FLOAT' => microtime(true),
42-
'REQUEST_URI' => $event->getUri(),
43-
], fn ($value) => null !== $value);
44-
45-
foreach ($event->getHeaders() as $name => $values) {
46-
$server['HTTP_'.strtoupper($name)] = $values[0];
47-
}
48-
49-
// TODO convert request better
50-
$request = Request::create(
51-
$event->getUri(),
52-
$event->getMethod(),
53-
[],
54-
[],
55-
[],
56-
$server,
57-
$event->getBody()
58-
);
59-
28+
$request = Request::createFromBase(SymfonyRequestBridge::convertRequest($event, $context));
6029
$response = $this->kernel->handle($request);
6130
$this->kernel->terminate($request, $response);
6231
$response->prepare($request);
6332

64-
return new HttpResponse($response->getContent(), $response->headers->all(), $response->getStatusCode());
33+
return SymfonyRequestBridge::convertResponse($response);
6534
}
6635
}

‎src/SymfonyHttpHandler.php

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Bref\Event\Http\HttpHandler;
77
use Bref\Event\Http\HttpRequestEvent;
88
use Bref\Event\Http\HttpResponse;
9-
use Symfony\Component\HttpFoundation\Request;
109
use Symfony\Component\HttpKernel\HttpKernelInterface;
1110
use Symfony\Component\HttpKernel\TerminableInterface;
1211

@@ -26,44 +25,13 @@ public function __construct(HttpKernelInterface $kernel)
2625

2726
public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse
2827
{
29-
Request::setTrustedProxies(['127.0.0.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO);
30-
31-
// CGI Version 1.1 - Section 4.1
32-
$server = array_filter([
33-
'AUTH_TYPE' => $event->getHeaders()['auth-type'] ?? null, // 4.1.1
34-
'CONTENT_LENGTH' => $event->getHeaders()['content-length'] ?? null, // 4.1.2
35-
'CONTENT_TYPE' => $event->getContentType(), // 4.1.3
36-
'QUERY_STRING' => $event->getQueryString(), // 4.1.7
37-
'REQUEST_METHOD' => $event->getMethod(), // 4.1.12
38-
'SERVER_PORT' => $event->getServerPort(), // 4.1.16
39-
'SERVER_PROTOCOL' => $event->getProtocolVersion(), // 4.1.16
40-
'DOCUMENT_ROOT' => getcwd(),
41-
'REQUEST_TIME' => time(),
42-
'REQUEST_TIME_FLOAT' => microtime(true),
43-
'REQUEST_URI' => $event->getUri(),
44-
], fn ($value) => null !== $value);
45-
46-
foreach ($event->getHeaders() as $name => $values) {
47-
$server['HTTP_'.strtoupper($name)] = $values[0];
48-
}
49-
50-
// TODO convert request better
51-
$request = Request::create(
52-
$event->getUri(),
53-
$event->getMethod(),
54-
[],
55-
[],
56-
[],
57-
$server,
58-
$event->getBody()
59-
);
60-
28+
$request = SymfonyRequestBridge::convertRequest($event, $context);
6129
$response = $this->kernel->handle($request);
6230

6331
if ($this->kernel instanceof TerminableInterface) {
6432
$this->kernel->terminate($request, $response);
6533
}
6634

67-
return new HttpResponse($response->getContent(), $response->headers->all(), $response->getStatusCode());
35+
return SymfonyRequestBridge::convertResponse($response);
6836
}
6937
}

‎src/SymfonyRequestBridge.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Runtime\Bref;
4+
5+
use Bref\Context\Context;
6+
use Bref\Event\Http\HttpRequestEvent;
7+
use Bref\Event\Http\HttpResponse;
8+
use Riverline\MultiPartParser\StreamedPart;
9+
use Symfony\Component\HttpFoundation\File\UploadedFile;
10+
use Symfony\Component\HttpFoundation\Request;
11+
use Symfony\Component\HttpFoundation\Response;
12+
13+
/**
14+
* Bridges Symfony requests and responses with API Gateway or ALB event/response
15+
* formats.
16+
*
17+
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
18+
*
19+
* @internal
20+
*/
21+
class SymfonyRequestBridge
22+
{
23+
public static function convertRequest(HttpRequestEvent $event, Context $context): Request
24+
{
25+
Request::setTrustedProxies(['127.0.0.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO);
26+
27+
// CGI Version 1.1 - Section 4.1
28+
$server = array_filter([
29+
'AUTH_TYPE' => $event->getHeaders()['auth-type'] ?? null, // 4.1.1
30+
'CONTENT_LENGTH' => $event->getHeaders()['content-length'][0] ?? null, // 4.1.2
31+
'CONTENT_TYPE' => $event->getContentType(), // 4.1.3
32+
'QUERY_STRING' => $event->getQueryString(), // 4.1.7
33+
'REQUEST_METHOD' => $event->getMethod(), // 4.1.12
34+
'SERVER_PORT' => $event->getServerPort(), // 4.1.16
35+
'SERVER_PROTOCOL' => 'HTTP/'.$event->getProtocolVersion(), // 4.1.16
36+
'DOCUMENT_ROOT' => getcwd(),
37+
'REQUEST_TIME' => time(),
38+
'REQUEST_TIME_FLOAT' => microtime(true),
39+
'REQUEST_URI' => $event->getUri(),
40+
'REMOTE_ADDR' => '127.0.0.1',
41+
], fn ($value) => null !== $value);
42+
43+
foreach ($event->getHeaders() as $name => $values) {
44+
$server['HTTP_'.strtoupper($name)] = $values[0];
45+
}
46+
47+
[$files, $parsedBody, $bodyString] = self::parseBodyAndUploadedFiles($event);
48+
49+
return new Request(
50+
$event->getQueryParameters(),
51+
$parsedBody ?? [],
52+
[], // Attributes
53+
$event->getCookies(),
54+
$files,
55+
$server,
56+
$bodyString
57+
);
58+
}
59+
60+
public static function convertResponse(Response $response): HttpResponse
61+
{
62+
return new HttpResponse($response->getContent(), $response->headers->all(), $response->getStatusCode());
63+
}
64+
65+
private static function parseBodyAndUploadedFiles(HttpRequestEvent $event): array
66+
{
67+
$bodyString = $event->getBody();
68+
$files = [];
69+
$parsedBody = null;
70+
$contentType = $event->getContentType();
71+
if (null !== $contentType && 'POST' === $event->getMethod()) {
72+
if ('application/x-www-form-urlencoded' === $contentType) {
73+
parse_str($bodyString, $parsedBody);
74+
} else {
75+
$stream = fopen('php://temp', 'rw');
76+
fwrite($stream, "Content-type: $contentType\r\n\r\n".$bodyString);
77+
rewind($stream);
78+
$document = new StreamedPart($stream);
79+
if ($document->isMultiPart()) {
80+
$bodyString = '';
81+
$parsedBody = [];
82+
foreach ($document->getParts() as $part) {
83+
if ($part->isFile()) {
84+
$tmpPath = tempnam(sys_get_temp_dir(), 'bref_upload_');
85+
if (false === $tmpPath) {
86+
throw new \RuntimeException('Unable to create a temporary directory');
87+
}
88+
file_put_contents($tmpPath, $part->getBody());
89+
if (0 !== filesize($tmpPath) && '' !== $part->getFileName()) {
90+
$file = new UploadedFile($tmpPath, $part->getFileName(), $part->getMimeType(), UPLOAD_ERR_OK, true);
91+
} else {
92+
$file = null;
93+
}
94+
95+
self::parseKeyAndInsertValueInArray($files, $part->getName(), $file);
96+
} else {
97+
self::parseKeyAndInsertValueInArray($parsedBody, $part->getName(), $part->getBody());
98+
}
99+
}
100+
}
101+
}
102+
}
103+
104+
return [$files, $parsedBody, $bodyString];
105+
}
106+
107+
/**
108+
* Parse a string key like "files[id_cards][jpg][]" and do $array['files']['id_cards']['jpg'][] = $value.
109+
*
110+
* @param mixed $value
111+
*/
112+
private static function parseKeyAndInsertValueInArray(array &$array, string $key, $value): void
113+
{
114+
if (false === strpos($key, '[')) {
115+
$array[$key] = $value;
116+
117+
return;
118+
}
119+
120+
$parts = explode('[', $key); // files[id_cards][jpg][] => [ 'files', 'id_cards]', 'jpg]', ']' ]
121+
$pointer = &$array;
122+
123+
foreach ($parts as $k => $part) {
124+
if (0 === $k) {
125+
$pointer = &$pointer[$part];
126+
127+
continue;
128+
}
129+
130+
// Skip two special cases:
131+
// [[ in the key produces empty string
132+
// [test : starts with [ but does not end with ]
133+
if ('' === $part || ']' !== substr($part, -1)) {
134+
// Malformed key, we use it "as is"
135+
$array[$key] = $value;
136+
137+
return;
138+
}
139+
140+
$part = substr($part, 0, -1); // The last char is a ] => remove it to have the real key
141+
142+
if ('' === $part) { // [] case
143+
$pointer = &$pointer[];
144+
} else {
145+
$pointer = &$pointer[$part];
146+
}
147+
}
148+
149+
$pointer = $value;
150+
}
151+
}

‎tests/.gitignore

Whitespace-only changes.

‎tests/SymfonyRequestBridgeTest.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace Runtime\Bref\Tests;
4+
5+
use Bref\Context\Context;
6+
use Bref\Event\Http\HttpRequestEvent;
7+
use PHPUnit\Framework\TestCase;
8+
use Runtime\Bref\SymfonyRequestBridge;
9+
use Symfony\Component\HttpFoundation\File\UploadedFile;
10+
11+
class SymfonyRequestBridgeTest extends TestCase
12+
{
13+
public function testClientIpFromForwardedFor()
14+
{
15+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
16+
'requestContext' => ['http' => ['method' => 'GET']],
17+
'multiValueHeaders' => ['x-forwarded-for' => ['98.76.54.32']],
18+
]), $this->getContext());
19+
$this->assertSame('98.76.54.32', $request->getClientIp());
20+
}
21+
22+
/**
23+
* Raw content should only exist when there is no multipart content.
24+
*/
25+
public function testRawContent()
26+
{
27+
// No content type
28+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
29+
'requestContext' => ['http' => ['method' => 'POST']],
30+
'body' => '{"foo":"bar"}',
31+
]), $this->getContext());
32+
$this->assertSame('{"foo":"bar"}', $request->getContent());
33+
34+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
35+
'requestContext' => ['http' => ['method' => 'POST']],
36+
'multiValueHeaders' => ['content-type' => ['application/json']],
37+
'body' => '{"foo":"bar"}',
38+
]), $this->getContext());
39+
$this->assertSame('{"foo":"bar"}', $request->getContent());
40+
41+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
42+
'requestContext' => ['http' => ['method' => 'POST']],
43+
'multiValueHeaders' => ['content-type' => ['application/x-www-form-urlencoded']],
44+
'body' => 'form%5Bname%5D=test&form%5Bsubmit%5D=',
45+
]), $this->getContext());
46+
$this->assertSame('form%5Bname%5D=test&form%5Bsubmit%5D=', $request->getContent());
47+
48+
// Multipart
49+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
50+
'requestContext' => ['http' => ['method' => 'POST']],
51+
'multiValueHeaders' => ['content-type' => ['multipart/form-data; boundary=----------------------------83ff53821b7c']],
52+
'body' => <<<HTTP
53+
------------------------------83ff53821b7c
54+
Content-Disposition: form-data; name="foo"
55+
56+
bar
57+
------------------------------83ff53821b7c
58+
Content-Disposition: form-data; name="rfc5987"; text1*=iso-8859-1'en'%A3%20rates; text2*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates
59+
60+
rfc
61+
------------------------------83ff53821b7c--
62+
63+
HTTP
64+
,
65+
]), $this->getContext());
66+
$this->assertSame('', $request->getContent());
67+
}
68+
69+
public function testUploadedFile()
70+
{
71+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
72+
'requestContext' => ['http' => ['method' => 'POST']],
73+
'multiValueHeaders' => ['content-type' => ['multipart/form-data; boundary=----------------------------83ff53821b7c']],
74+
'body' => <<<HTTP
75+
------------------------------83ff53821b7c
76+
Content-Disposition: form-data; name="form[img]"; filename="a.png"
77+
Content-Type: image/png
78+
79+
?PNG
80+
81+
IHD?wS??iCCPICC Profilex?T?kA?6n??Zk?x?"IY?hE?6?bk
82+
Y?<ߡ)??????9Nyx?+=?Y"|@5-?M?S?%?@?H8??qR>?׋??inf???O?????b??N?????~N??>?!?
83+
??V?J?p?8?da?sZHO?Ln?}&???wVQ?y?g????E??0
84+
??
85+
IDAc????????-IEND?B`?
86+
------------------------------83ff53821b7c
87+
Content-Disposition: form-data; name="foo"
88+
89+
bar
90+
------------------------------83ff53821b7c--
91+
92+
HTTP
93+
,
94+
]), $this->getContext());
95+
$files = $request->files->all();
96+
$this->assertArrayHasKey('img', $files['form']);
97+
$this->assertInstanceOf(UploadedFile::class, $files['form']['img']);
98+
/** @var UploadedFile $file */
99+
$file = $files['form']['img'];
100+
$this->assertSame('a.png', $file->getClientOriginalName());
101+
$this->assertSame('image/png', $file->getClientMimeType());
102+
$this->assertSame(UPLOAD_ERR_OK, $file->getError());
103+
104+
$post = $request->request->all();
105+
$this->assertArrayHasKey('foo', $post);
106+
$this->assertSame('bar', $post['foo']);
107+
}
108+
109+
public function testEmptyUploadedFile()
110+
{
111+
$request = SymfonyRequestBridge::convertRequest(new HttpRequestEvent([
112+
'requestContext' => ['http' => ['method' => 'POST']],
113+
'multiValueHeaders' => ['content-type' => ['multipart/form-data; boundary=----------------------------83ff53821b7c']],
114+
'body' => <<<HTTP
115+
------------------------------83ff53821b7c
116+
Content-Disposition: form-data; name="form[img]"; filename=""
117+
Content-Type: application/octet-stream
118+
119+
120+
------------------------------83ff53821b7c
121+
Content-Disposition: form-data; name="foo"
122+
123+
bar
124+
------------------------------83ff53821b7c--
125+
126+
HTTP
127+
,
128+
]), $this->getContext());
129+
$files = $request->files->all();
130+
$this->assertArrayHasKey('img', $files['form']);
131+
$this->assertNull($files['form']['img']);
132+
133+
$post = $request->request->all();
134+
$this->assertArrayHasKey('foo', $post);
135+
$this->assertSame('bar', $post['foo']);
136+
}
137+
138+
private function getContext()
139+
{
140+
return new Context('id', 1000, 'function', 'trace');
141+
}
142+
}

0 commit comments

Comments
 (0)
Please sign in to comment.