Skip to content

Commit ecbd968

Browse files
committed
Support Chunked-Encoding remote zip files. Ship a small test server
1 parent 96ab01b commit ecbd968

19 files changed

+548
-14
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ blueprint-url-updater.json
2929
components/DataLiberation/Tests/test-output-html/
3030
components/DataLiberation/Tests/test-output-md
3131
blueprint-dev.json
32-
examples/create-wp-site/data-liberation.zip
32+
examples/create-wp-site/data-liberation.zip
33+
untracked

components/Git/GitEndpoint.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use WordPress\Git\Protocol\GitProtocolEncoderPipe;
88
use WordPress\Git\Protocol\Parser\GitProtocolDecoder;
99
use WordPress\Git\Protocol\Parser\PacketParser;
10-
use WordPress\HttpServer\ResponseWriter\ResponseWriteStream;
10+
use WordPress\HttpServer\Response\ResponseWriteStream;
1111

1212
/**
1313
* Implement Git server protocol v2

components/Git/Tests/GitServerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use WordPress\Git\Model\TreeEntry;
1212
use WordPress\Git\Protocol\GitProtocolEncoderPipe;
1313
use WordPress\Git\Protocol\Parser\GitProtocolDecoder;
14-
use WordPress\HttpServer\ResponseWriter\BufferingResponseWriter;
14+
use WordPress\HttpServer\Response\BufferingResponseWriter;
1515

1616
class GitServerTest extends TestCase {
1717

components/HttpClient/ByteStream/ChunkedDecoderReadStream.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace WordPress\HttpClient\ByteStream;
44

55
use WordPress\ByteStream\ReadStream\BaseByteReadStream;
6-
use WordPress\ByteStream\ReadStream\ByteReadStream;
76

87
class ChunkedDecoderReadStream extends BaseByteReadStream {
98

components/HttpClient/ByteStream/ChunkedEncoderByteTransformer.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ class ChunkedEncoderByteTransformer implements ByteTransformer {
1414
* @param string $bytes The bytes to encode.
1515
*/
1616
public function filter_bytes( $bytes ): string {
17-
$chunk_size = str_pad( dechex( strlen( $bytes ) ), 2, '0', STR_PAD_LEFT );
17+
if(strlen($bytes) === 0) {
18+
return "";
19+
}
20+
$chunk_size = strtoupper( dechex( strlen( $bytes ) ) );
1821
return $chunk_size . "\r\n" . $bytes . "\r\n";
1922
}
2023

components/HttpClient/ByteStream/RequestReadStream.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ private function pull_until_event( $options = array() ) {
143143
}
144144
break;
145145
case Client::EVENT_FINISHED:
146+
/**
147+
* If the server did not provide a Content-Length header,
148+
* backfill the file length with the number of downloaded
149+
* bytes.
150+
*/
151+
if(null === $this->remote_file_length) {
152+
$this->remote_file_length = $this->bytes_already_forgotten + strlen($this->buffer);
153+
}
146154
return '';
147155
case Client::EVENT_FAILED:
148156
// TODO: Think through error handling. Errors are expected when working with

components/HttpClient/Request.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function __construct( string $url, $request_info = array() ) {
8585
$this->method = $request_info['method'];
8686

8787
$headers = array(
88-
'host' => $url_parts['host'],
88+
'host' => isset($url_parts['host']) ? $url_parts['host'] : '',
8989
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36',
9090
'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
9191
'accept-language' => 'en-US,en;q=0.9',
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace WordPress\HttpServer;
4+
5+
use Rowbot\URL\URL;
6+
use WordPress\ByteStream\ReadStream\ByteReadStream;
7+
use WordPress\ByteStream\ReadStream\FileReadStream;
8+
use WordPress\ByteStream\ReadStream\InflateReadStream;
9+
use WordPress\DataLiberation\URL\WPURL;
10+
use WordPress\HttpClient\Request;
11+
12+
class IncomingRequest extends Request {
13+
14+
static public function from_resource( $upstream ) {
15+
// Read request line
16+
$line = fgets( $upstream );
17+
if ( $line === false ) {
18+
throw new \Exception("Failed to read request line");
19+
}
20+
$parts = explode( ' ', trim( $line ), 3 );
21+
$request_info = [
22+
'method' => $parts[0] ?? 'GET',
23+
'pathname' => $parts[1] ?? '/',
24+
'headers' => [],
25+
];
26+
27+
// Read headers
28+
while ( ( $line = fgets( $upstream ) ) !== false ) {
29+
$line = trim( $line );
30+
if ( $line === '' ) {
31+
break;
32+
}
33+
$header_parts = explode( ':', $line, 2 );
34+
if ( count( $header_parts ) == 2 ) {
35+
$name = strtolower(trim($header_parts[0]));
36+
$request_info['headers'][ $name ] = trim($header_parts[1]);
37+
}
38+
}
39+
40+
// @TODOL: Validate the Host, URL, throw an error if invalid
41+
$request = new IncomingRequest(
42+
// @TODO: figure out protocol
43+
'http://' . ($request_info['headers']['host'] ?? 'unknown-host') . $request_info['pathname'],
44+
$request_info
45+
);
46+
47+
$body_stream = FileReadStream::from_resource( $upstream );
48+
49+
$wrapped_streams = [];
50+
51+
$encoding = $request->get_header('Content-Encoding');
52+
if($encoding) {
53+
foreach(explode(',', $encoding) as $encoding) {
54+
$encoding = trim($encoding);
55+
switch($encoding) {
56+
case 'gzip':
57+
$wrapped_streams[] = $body_stream;
58+
$body_stream = new InflateReadStream( $body_stream, ZLIB_ENCODING_GZIP );
59+
break;
60+
case 'deflate':
61+
$wrapped_streams[] = $body_stream;
62+
$body_stream = new InflateReadStream( $body_stream );
63+
break;
64+
default:
65+
throw new \Exception("Unsupported content encoding: {$encoding}");
66+
}
67+
}
68+
}
69+
70+
// Support chunked transfer decoding
71+
$transfer_encoding = $request->get_header('transfer-encoding');
72+
if ($transfer_encoding) {
73+
foreach (explode(',', $transfer_encoding) as $te) {
74+
$te = strtolower(trim($te));
75+
switch ($te) {
76+
case 'chunked':
77+
$wrapped_streams[] = $body_stream;
78+
$body_stream = new \WordPress\HttpClient\ByteStream\ChunkedDecoderReadStream($body_stream);
79+
break;
80+
// You can add support for other transfer-encodings here if needed
81+
default:
82+
// Ignore or throw for unknown encodings if desired
83+
break;
84+
}
85+
}
86+
}
87+
88+
$request->body_stream = $body_stream;
89+
$request->wrapped_streams = $wrapped_streams;
90+
91+
return $request;
92+
}
93+
94+
public ByteReadStream $body_stream;
95+
private array $wrapped_streams;
96+
private ?URL $parsed_url = null;
97+
98+
// @TODO: Bake this into the body stream instance
99+
public function close_body_stream()
100+
{
101+
// Do not close $this->body_stream as it would close
102+
// the tcp connection with the client. We need that
103+
// connection to send the response.
104+
foreach($this->wrapped_streams as $stream) {
105+
$stream->close_reading();
106+
}
107+
}
108+
109+
public function get_parsed_url() {
110+
if(null === $this->parsed_url) {
111+
$parsed_url = WPURL::parse($this->url);
112+
if(false === $parsed_url) {
113+
throw new \Exception("Invalid URL: {$this->url}");
114+
}
115+
$this->parsed_url = $parsed_url;
116+
}
117+
return $this->parsed_url;
118+
}
119+
}

components/HttpServer/ResponseWriter/BufferingResponseWriter.php renamed to components/HttpServer/Response/BufferingResponseWriter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace WordPress\HttpServer\ResponseWriter;
3+
namespace WordPress\HttpServer\Response;
44

55
class BufferingResponseWriter implements ResponseWriteStream {
66

components/HttpServer/ResponseWriter/ResponseWriteStream.php renamed to components/HttpServer/Response/ResponseWriteStream.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace WordPress\HttpServer\ResponseWriter;
3+
namespace WordPress\HttpServer\Response;
44

55
use WordPress\ByteStream\WriteStream\ByteWriteStream;
66

components/HttpServer/ResponseWriter/StreamingResponseWriter.php renamed to components/HttpServer/Response/StreamingResponseWriter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace WordPress\HttpServer\ResponseWriter;
3+
namespace WordPress\HttpServer\Response;
44

55
class StreamingResponseWriter implements ResponseWriteStream {
66

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace WordPress\HttpServer\Response;
4+
5+
use WordPress\ByteStream\WriteStream\ByteWriteStream;
6+
use WordPress\ByteStream\WriteStream\TransformedWriteStream;
7+
use WordPress\HttpClient\ByteStream\ChunkedEncoderByteTransformer;
8+
use WordPress\HttpServer\StatusCode;
9+
10+
class TcpResponseWriteStream implements ResponseWriteStream
11+
{
12+
/**
13+
* @var ByteWriteStream
14+
*/
15+
private $upstream;
16+
17+
/**
18+
* @var int
19+
*/
20+
public $http_code = 200;
21+
22+
/**
23+
* @var array
24+
*/
25+
private $headers = [];
26+
27+
/**
28+
* @var bool
29+
*/
30+
private $headers_sent = false;
31+
32+
/**
33+
* @var bool
34+
*/
35+
private $closed = false;
36+
37+
/**
38+
* @var ByteWriteStream
39+
*/
40+
private $writer;
41+
42+
public function __construct(ByteWriteStream $upstream)
43+
{
44+
$this->upstream = $upstream;
45+
$this->writer = new TransformedWriteStream($this->upstream, []);
46+
}
47+
48+
public function send_http_code($code)
49+
{
50+
if ($this->headers_sent) {
51+
throw new \RuntimeException("Cannot set HTTP code after headers have been sent");
52+
}
53+
$this->http_code = (int)$code;
54+
}
55+
56+
public function send_header($name, $value)
57+
{
58+
if ($this->headers_sent) {
59+
throw new \RuntimeException("Cannot send header after headers have been sent");
60+
}
61+
$lname = strtolower($name);
62+
$this->headers[$lname] = [$name, $value];
63+
}
64+
65+
public function send_headers_if_needed()
66+
{
67+
if ($this->headers_sent) {
68+
return;
69+
}
70+
71+
// Status line
72+
$status_text = StatusCode::text($this->http_code);
73+
$this->upstream->append_bytes("HTTP/1.1 {$this->http_code} {$status_text}\r\n");
74+
75+
// Headers
76+
foreach ($this->headers as [$name, $value]) {
77+
$this->upstream->append_bytes("{$name}: {$value}\r\n");
78+
}
79+
80+
// End of headers
81+
$this->upstream->append_bytes("\r\n");
82+
83+
$this->headers_sent = true;
84+
}
85+
86+
public function use_chunked_encoding()
87+
{
88+
$this->writer['chunked'] = new ChunkedEncoderByteTransformer();
89+
$this->send_header( 'Transfer-Encoding', 'chunked' );
90+
}
91+
92+
public function append_bytes(string $bytes): void
93+
{
94+
if ($this->closed) {
95+
throw new \RuntimeException("Cannot write to closed ResponseWriteStream");
96+
}
97+
$this->send_headers_if_needed();
98+
$this->writer->append_bytes($bytes);
99+
}
100+
101+
public function is_writing_closed(): bool
102+
{
103+
return $this->closed;
104+
}
105+
106+
public function close_writing(): void
107+
{
108+
if ($this->closed) {
109+
throw new \RuntimeException("ResponseWriteStream already closed");
110+
}
111+
$this->send_headers_if_needed();
112+
$this->writer->close_writing();
113+
$this->closed = true;
114+
}
115+
116+
}

0 commit comments

Comments
 (0)