5
5
use WordPress \ByteStream \ByteTransformer \InflateTransformer ;
6
6
use WordPress \ByteStream \ReadStream \FileReadStream ;
7
7
use WordPress \ByteStream \ReadStream \TransformedReadStream ;
8
+ use WordPress \DataLiberation \URL \WPURL ;
8
9
use WordPress \HttpClient \ByteStream \ChunkedDecoderReadStream ;
9
10
use WordPress \HttpClient \ByteStream \ChunkedEncoderByteTransformer ;
10
11
use WordPress \HttpClient \ByteStream \RequestReadStream ;
@@ -107,13 +108,13 @@ class Client {
107
108
protected $ event ;
108
109
protected $ request ;
109
110
protected $ response_body_chunk ;
110
- protected $ timeout ;
111
+ protected $ request_timeout_ms ;
111
112
protected $ requests_started_at = array ();
112
113
113
114
public function __construct ( $ options = array () ) {
114
115
$ this ->concurrency = $ options ['concurrency ' ] ?? 10 ;
115
116
$ this ->max_redirects = $ options ['max_redirects ' ] ?? 3 ;
116
- $ this ->timeout = $ options ['timeout ' ] ?? 30 ;
117
+ $ this ->request_timeout_ms = $ options ['timeout_ms ' ] ?? 30000 ;
117
118
$ this ->requests = array ();
118
119
}
119
120
@@ -162,10 +163,28 @@ public function enqueue( $requests ) {
162
163
}
163
164
164
165
foreach ( $ requests as $ request ) {
166
+ if ( array_key_exists ( $ request ->id , $ this ->connections ) ) {
167
+ throw new HttpClientException ("Request {$ request ->id } is already enqueued. " );
168
+ }
169
+
170
+ if ($ request ->state !== Request::STATE_CREATED ) {
171
+ throw new HttpClientException ("Request {$ request ->id } is not in the created state. " );
172
+ }
173
+
174
+ $ request ->state = Request::STATE_ENQUEUED ;
165
175
$ this ->requests [] = apply_filters ( 'wp_http_client_request ' , $ request );
166
176
$ this ->events [ $ request ->id ] = array ();
167
177
$ this ->connections [ $ request ->id ] = new Connection ( $ request );
168
- $ this ->requests_started_at [ $ request ->id ] = microtime ( true );
178
+
179
+ $ parsed = WPURL ::parse ($ request ->url );
180
+ if (false === $ parsed ) {
181
+ $ this ->set_error ( $ request , new HttpError ( sprintf ( 'Invalid URL: %s ' , $ request ->url ) ) );
182
+ continue ;
183
+ }
184
+ if ($ parsed ->protocol !== 'http: ' && $ parsed ->protocol !== 'https: ' ) {
185
+ $ this ->set_error ( $ request , new HttpError ( sprintf ( 'Invalid URL – only HTTP and HTTPS URLs are supported: %s ' , $ parsed ->toString () ) ) );
186
+ continue ;
187
+ }
169
188
}
170
189
}
171
190
@@ -237,13 +256,13 @@ public function await_next_event( $query = array() ) {
237
256
$ this ->response_body_chunk = null ;
238
257
239
258
$ start_time = microtime ( true );
240
- $ timeout = $ query ['timeout ' ] ?? $ this ->timeout * 1000 ;
259
+ $ timeout_ms = isset ( $ query ['timeout_ms ' ] )
260
+ ? $ query ['timeout_ms ' ]
261
+ // Give the requests an opportunity to time out
262
+ : $ this ->request_timeout_ms * 1.1
263
+ ;
241
264
242
265
do {
243
- if ( false !== $ timeout && ( microtime ( true ) - $ start_time ) * 1000 >= $ timeout ) {
244
- return false ;
245
- }
246
-
247
266
if ( empty ( $ query ['requests ' ] ) ) {
248
267
$ events = array_keys ( $ this ->events );
249
268
} else {
@@ -275,6 +294,15 @@ public function await_next_event( $query = array() ) {
275
294
return true ;
276
295
}
277
296
}
297
+
298
+ // After we've checked for any available events, see if we've run out of time.
299
+ // This way, we always return any events that were ready before worrying about the timeout.
300
+ // If we checked the timeout first, we might miss events that were already waiting for us
301
+ // when the timeout is set to zero.
302
+ $ time_elapsed_ms = (microtime ( true ) - $ start_time ) * 1000 ;
303
+ if ( $ timeout_ms && $ time_elapsed_ms >= $ timeout_ms ) {
304
+ return false ;
305
+ }
278
306
} while ( $ this ->event_loop_tick () );
279
307
280
308
return false ;
@@ -336,9 +364,18 @@ protected function event_loop_tick() {
336
364
return false ;
337
365
}
338
366
339
- foreach ( $ this ->get_active_requests () as $ request ) {
340
- if ( microtime ( true ) - $ this ->requests_started_at [ $ request ->id ] > $ this ->timeout ) {
341
- $ this ->set_error ( $ request , new HttpError ( sprintf ( 'Request timed out after %s seconds. ' , $ this ->timeout ) ) );
367
+ foreach ( $ this ->get_active_requests ([
368
+ Request::STATE_WILL_ENABLE_CRYPTO ,
369
+ Request::STATE_WILL_SEND_HEADERS ,
370
+ Request::STATE_WILL_SEND_BODY ,
371
+ Request::STATE_SENT ,
372
+ Request::STATE_RECEIVING_HEADERS ,
373
+ Request::STATE_RECEIVING_BODY ,
374
+ Request::STATE_RECEIVED ,
375
+ ]) as $ request ) {
376
+ $ time_elapsed_ms = (microtime ( true ) - $ this ->requests_started_at [ $ request ->id ]) * 1000 ;
377
+ if ( $ time_elapsed_ms > $ this ->request_timeout_ms ) {
378
+ $ this ->set_error ( $ request , new HttpError ( sprintf ( 'Request timed out after %s seconds. ' , $ time_elapsed_ms ) ) );
342
379
}
343
380
}
344
381
@@ -390,6 +427,7 @@ protected function event_loop_tick() {
390
427
$ this ->get_active_requests ( Request::STATE_RECEIVING_BODY )
391
428
);
392
429
430
+
393
431
return true ;
394
432
}
395
433
@@ -444,7 +482,7 @@ protected function close_connection( Request $request ) {
444
482
if ( $ this ->connections [ $ request ->id ]->decoded_response_stream ) {
445
483
$ stream = $ this ->connections [ $ request ->id ]->decoded_response_stream ;
446
484
$ stream ->close_reading ();
447
- unset( $ this ->connections [ $ request ->id ]->decoded_response_stream ) ;
485
+ $ this ->connections [ $ request ->id ]->decoded_response_stream = null ;
448
486
} else {
449
487
@fclose ( $ socket );
450
488
}
@@ -533,6 +571,9 @@ protected function decode_and_monitor_response_body_stream( Request $request ) {
533
571
$ transfer_encoding === 'gzip ' ? ZLIB_ENCODING_GZIP : ZLIB_ENCODING_RAW
534
572
);
535
573
break ;
574
+ case 'deflate ' :
575
+ $ transformers [] = new InflateTransformer ( ZLIB_ENCODING_DEFLATE );
576
+ break ;
536
577
case 'identity ' :
537
578
// No-op
538
579
break ;
@@ -560,16 +601,17 @@ protected function decode_and_monitor_response_body_stream( Request $request ) {
560
601
*/
561
602
protected function enable_crypto ( array $ requests ) {
562
603
foreach ( $ this ->stream_select ( $ requests , static ::STREAM_SELECT_WRITE ) as $ request ) {
563
- stream_set_timeout ( $ this ->connections [ $ request ->id ]->http_socket , 1 );
604
+ @ stream_set_timeout ( $ this ->connections [ $ request ->id ]->http_socket , 1 );
564
605
$ enabled_crypto = stream_socket_enable_crypto (
565
606
$ this ->connections [ $ request ->id ]->http_socket ,
566
607
true ,
567
- STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
608
+ STREAM_CRYPTO_METHOD_TLS_CLIENT
568
609
);
569
610
if ( false === $ enabled_crypto ) {
570
611
$ last_error = error_get_last ();
571
612
$ this ->set_error ( $ request ,
572
- new HttpError ( 'Failed to enable crypto: ' . ( is_array ( $ last_error ) ? $ last_error ['message ' ] : 'unknown ' ) ) );
613
+ new HttpError ( 'Failed to enable crypto: ' . ( is_array ( $ last_error ) ? $ last_error ['message ' ] : 'unknown ' ) )
614
+ );
573
615
continue ;
574
616
} elseif ( 0 === $ enabled_crypto ) {
575
617
// The SSL handshake isn't finished yet, let's skip it
@@ -589,11 +631,12 @@ protected function enable_crypto( array $requests ) {
589
631
protected function send_request_headers ( array $ requests ) {
590
632
foreach ( $ this ->stream_select ( $ requests , static ::STREAM_SELECT_WRITE ) as $ request ) {
591
633
$ header_bytes = static ::prepare_request_headers ( $ request );
592
-
593
634
if ( false === @fwrite ( $ this ->connections [ $ request ->id ]->http_socket , $ header_bytes ) ) {
594
635
$ last_error = error_get_last ();
636
+ $ last_error_message = is_array ( $ last_error ) ? $ last_error ['message ' ] : 'unknown ' ;
595
637
$ this ->set_error ( $ request ,
596
- new HttpError ( 'Failed to write request bytes - ' . ( is_array ( $ last_error ) ? $ last_error ['message ' ] : 'unknown ' ) ) );
638
+ new HttpError ( 'Failed to write request bytes - ' . $ last_error_message )
639
+ );
597
640
continue ;
598
641
}
599
642
@@ -635,8 +678,10 @@ protected function send_request_body( array $requests ) {
635
678
}
636
679
637
680
$ chunk = $ request ->upload_body_stream ->consume ( $ available_bytes );
638
- if ( ! fwrite ( $ this ->connections [ $ request ->id ]->http_socket , $ chunk ) ) {
639
- $ this ->set_error ( $ request , new HttpError ( 'Failed to write request bytes. ' ) );
681
+ if ( ! @fwrite ( $ this ->connections [ $ request ->id ]->http_socket , $ chunk ) ) {
682
+ $ last_error = error_get_last ();
683
+ $ last_error_message = is_array ( $ last_error ) ? $ last_error ['message ' ] : 'unknown ' ;
684
+ $ this ->set_error ( $ request , new HttpError ( 'Failed to write request bytes: ' . $ last_error_message ) );
640
685
continue ;
641
686
}
642
687
}
@@ -660,11 +705,22 @@ protected function receive_response_headers( $requests ) {
660
705
while ( true ) {
661
706
// @TODO: Use a larger chunk size here and then scan for \r\n\r\n.
662
707
// 1 seems slow and overly conservative.
708
+ if (
709
+ !$ this ->connections [ $ request ->id ]->http_socket ||
710
+ !is_resource ($ this ->connections [ $ request ->id ]->http_socket ) ||
711
+ @feof ($ this ->connections [ $ request ->id ]->http_socket )
712
+ ) {
713
+ $ this ->set_error ($ request , new HttpError ('Connection closed while reading response headers. ' ));
714
+ break ;
715
+ }
716
+
663
717
$ header_byte = fread ( $ this ->connections [ $ request ->id ]->http_socket , 1 );
718
+
664
719
if ( false === $ header_byte || '' === $ header_byte ) {
665
720
if (
666
- !is_resource ($ this ->connections [ $ request ->id ]->http_socket ) ||
667
- feof ($ this ->connections [ $ request ->id ]->http_socket )
721
+ !$ this ->connections [ $ request ->id ]->http_socket ||
722
+ !is_resource ($ this ->connections [ $ request ->id ]->http_socket ) ||
723
+ @feof ($ this ->connections [ $ request ->id ]->http_socket )
668
724
) {
669
725
$ this ->set_error ($ request , new HttpError ('Connection closed while reading response headers. ' ));
670
726
break ;
@@ -709,13 +765,8 @@ protected function receive_response_headers( $requests ) {
709
765
break ;
710
766
}
711
767
712
- // If we're being redirected, we don't need to wait for the body.
713
- if ( $ response ->status_code >= 300 && $ response ->status_code < 400 ) {
714
- $ request ->state = Request::STATE_RECEIVED ;
715
- break ;
716
- }
717
-
718
768
$ request ->state = Request::STATE_RECEIVING_BODY ;
769
+ $ this ->connections [ $ request ->id ]->decoded_response_stream = $ this ->decode_and_monitor_response_body_stream ( $ request );
719
770
break ;
720
771
}
721
772
}
@@ -734,10 +785,6 @@ protected function receive_response_body( $requests ) {
734
785
// * The last chunk in Transfer-Encoding: chunked is received
735
786
// * The connection is closed
736
787
foreach ( $ this ->stream_select ( $ requests , static ::STREAM_SELECT_READ ) as $ request ) {
737
- if ( ! $ this ->connections [ $ request ->id ]->decoded_response_stream ) {
738
- $ this ->connections [ $ request ->id ]->decoded_response_stream = $ this ->decode_and_monitor_response_body_stream ( $ request );
739
- }
740
-
741
788
$ stream = $ this ->connections [ $ request ->id ]->decoded_response_stream ;
742
789
743
790
while ( true ) {
@@ -789,30 +836,20 @@ protected function handle_redirects( $requests ) {
789
836
}
790
837
791
838
$ redirect_url = $ location ;
792
- if ( strpos ( $ redirect_url , 'http:// ' ) !== 0 && strpos ( $ redirect_url , 'https:// ' ) !== 0 ) {
793
- $ current_url_parts = parse_url ( $ request ->url );
794
- $ redirect_url = $ current_url_parts ['scheme ' ] . ':// ' . $ current_url_parts ['host ' ];
795
- if ( $ current_url_parts ['port ' ] ) {
796
- $ redirect_url .= ': ' . $ current_url_parts ['port ' ];
797
- }
798
- if ( strlen ( $ location ) === 0 || $ location [0 ] !== '/ ' ) {
799
- $ redirect_url .= '/ ' ;
800
- }
801
- $ redirect_url .= $ location ;
802
- }
803
-
804
- // @TODO: Use a WHATWG-compliant URL parser
805
- if ( ! filter_var ( $ redirect_url , FILTER_VALIDATE_URL ) ) {
806
- $ this ->set_error ( $ request , new HttpError ( 'Invalid redirect URL ' ) );
839
+ $ parsed = WPURL ::parse ($ redirect_url , $ request ->url );
840
+ if (false === $ parsed ) {
841
+ $ this ->set_error ( $ request , new HttpError ( sprintf ( 'Invalid redirect URL: %s ' , $ redirect_url ) ) );
807
842
continue ;
808
843
}
844
+ $ redirect_url = $ parsed ->toString ();
809
845
810
846
$ this ->events [ $ request ->id ][ self ::EVENT_REDIRECT ] = true ;
811
847
$ this ->enqueue (
812
848
new Request (
813
849
$ redirect_url ,
814
850
array (
815
- 'method ' => $ request ->method ,
851
+ // Redirects are always GET requests
852
+ 'method ' => 'GET ' ,
816
853
'redirected_from ' => $ request ,
817
854
)
818
855
)
@@ -904,11 +941,12 @@ protected function open_nonblocking_http_sockets( $requests ) {
904
941
)
905
942
);
906
943
944
+ $ this ->requests_started_at [ $ request ->id ] = microtime ( true );
907
945
$ stream = @stream_socket_client (
908
946
'tcp:// ' . $ host . ': ' . $ port ,
909
947
$ errno ,
910
948
$ errstr ,
911
- $ this ->timeout ,
949
+ $ this ->request_timeout_ms ,
912
950
STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT ,
913
951
$ context
914
952
);
@@ -921,13 +959,7 @@ protected function open_nonblocking_http_sockets( $requests ) {
921
959
continue ;
922
960
}
923
961
924
- if ( PHP_VERSION_ID >= 72000 ) {
925
- // In PHP <= 7.1 and later, making the socket non-blocking before the
926
- // SSL handshake makes the stream_socket_enable_crypto() call always return
927
- // false. Therefore, we only make the socket non-blocking after the
928
- // SSL handshake.
929
- stream_set_blocking ( $ stream , 0 );
930
- }
962
+ stream_set_blocking ( $ stream , false );
931
963
932
964
$ this ->connections [ $ request ->id ]->http_socket = $ stream ;
933
965
if ( $ is_ssl ) {
0 commit comments