php:无法在php://输入上执行多个fread()调用

时间:2017-05-29 14:41:56

标签: php apache

我通过POST使用content-encoding: chunked来回传送数据到PHP应用程序。我需要我的PHP应用程序来读取一些数据,处理它,发回响应,读取更多数据等等。我无法一次读取所有数据,因为它不可用。想象一下大型文件上传,校验和会定期作为响应发送。

问题在于,虽然我可以从php://input读取少量字节,但后续调用fread不会返回新内容。

目前我正在使用PHP's Docker container。我尝试了同样的结果php:7.0-apachephp:5-apache

下面的PoC客户端生成随机字符串,并以3秒的间隔将它们作为块发送到服务器。服务器以1秒的间隔从php://input读取并打印内容。服务器输出只显示前三个字符串被读取;服务器似乎也会“阻塞”,直到读完前三个。

我尝试过的事情无济于事:

  • 使用fseek
  • 使用stream_select似乎无效,呃,php://input 。我不知道为什么这对我来说是理想的,但考虑到PHP的设计和实现有多糟糕,我并不感到惊讶。
  • 关闭并重新开启php://input
  • 使用fgetc

客户端输出:

    $ python poc.py
Sending:
---
POST /poc.php HTTP/1.1
Host: localhost
accept-encoding: *;q=0
Transfer-Encoding: chunked
Content-Type: application/octet-stream


---

After sending headers, response:
 HTTP/1.1 200 OK
Date: Mon, 29 May 2017 14:25:52 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/5.6.30
transfer-encoding: chunked
Content-Type: application/octet-stream

4
OK


Waiting 3 seconds
Sending string: AuVuvsyGJc

Waiting 3 seconds
Sending string: LfKouYzccV

Waiting 3 seconds
Sending string: WmpPspYqiR

Waiting 3 seconds
Sending string: IApMOjoaIv

Waiting 3 seconds
Sending string: tuGrVklcVy

Waiting 3 seconds
Sending string: btUVIezCow

Waiting 3 seconds
Sending string: XUPOrEidyd

Traceback (most recent call last):
  File "poc.py", line 33, in <module>
    websock.send(to_chunk(rnd))
socket.error: [Errno 32] Broken pipe

服务器输出:

Connected
Read: AuVuvsyGJc
LfKouYzccV
WmpPspYqiR

Read:
Read:
Read:
Read:
172.17.0.1 - - [29/May/2017:14:25:52 +0000] "POST /poc.php HTTP/1.1" 200 191 "-" "-"

PHP服务器:

<?php
header("transfer-encoding: chunked");
header("content-type: application/octet-stream");
flush(); 
/**
 * Useful to print debug messages in the Apache logs
 */
function _log($what) {
    file_put_contents("php://stderr", print_r($what, true) . "\n");
}
_log("Connected");

/**
 * To send data as chunks
 */
function _ch($chunk) {
    echo sprintf("%x\r\n", strlen($chunk));
    echo $chunk;
    echo "\r\n";
    flush();
}
// Test chunks
_ch("OK\r\n");

$web_php_input = fopen("php://input", 'r');
$continue = 5;
while ($continue-- > 0) {
    $contents = fread($web_php_input, 1024);
    _log("Read: " . $contents);
    sleep(1);
}
fclose($web_php_input);
?>

Python客户端:

from __future__ import print_function
import random
import socket
import string
import time

def to_chunk(what):
    return format(len(what), 'X') + "\r\n" + what + "\r\n"

websock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
websock.connect(("localhost", 8080))

# Send the initial chunked POST header
connect_string = ''.join((
    "POST /poc.php HTTP/1.1\r\n",
    "Host: localhost\r\n",
    "accept-encoding: *;q=0\r\n",  # ,gzip;q=0,deflate;q=0\r\n",
    "Transfer-Encoding: chunked\r\n",
    "Content-Type: application/octet-stream\r\n",
    # "Connection: keep-alive\r\n",
    "\r\n",
))
print("Sending:\n---\n{}\n---\n".format(connect_string))
websock.sendall(connect_string)
print("After sending headers, response:\n {}".format(websock.recv(1024)))
c = True
while c:
    print("Waiting 3 seconds")
    time.sleep(3)
    rnd = ''.join(random.choice(string.ascii_letters) for _ in range(10))
    rnd += '\r\n'
    print("Sending string: {}".format(rnd))
    websock.send(to_chunk(rnd))
print("done")

Dockerfile:

FROM php:5-apache
COPY custom.ini /usr/local/etc/php/conf.d

Docker命令行:

docker build -t listener .
docker run -i --rm -p 8080:80 -v $(pwd):/var/www/html --name listener listener

custom.ini文件让PHP知道POST主体不应该被缓冲:

enable_post_data_reading=false

在其他人建议使用其他语言,框架或以不同方式做事之前:它必须是PHP;它不能依赖任何第三方库或PECL;这正是我的需要。

作为旁注,此行为符合the HTTP spec;在将部分响应返回给客户端之前,服务器不必读取所有入站数据。另请参阅RFC6202

1 个答案:

答案 0 :(得分:5)

为了理解为什么会这样,你需要知道HTTP是如何工作的,遗憾的是,这不是你认为的那样。 分块传输编码和PHP也不像你认为的那样工作。 我会尝试以与我认为你要做的事情相关的方式解释。

如果我理解正确,您将尝试以交错方式发送请求和响应的块,或者在您描述时来回发送数据。 这违反了HTTP规范。 因此,您无法做到这一点,因为请求是由HTTP服务器直接处理的,而不是PHP。

HTTP

HTTP是一种请求/响应协议(RFC2616第1.4节),它具有简单的操作:

  1. 客户端向服务器发送HTTP请求消息。
  2. 接收并解释请求消息后,服务器会响应HTTP响应消息。 (RFC2616第6节)
  3. 请注意,第2步表示&#34;在&#34;之后&#34;在&#34;,这意味着服务器必须等待请求完成才能发送响应。 这就是为什么&#34;服务器似乎阻止&#34;。

    RFC6202中描述的HTTP长轮询和HTTP流的生命周期实际上以相同的方式工作,没有违反HTTP规范。 他们不会来回发送数据(没有交错)。

    Chunked Transfer-Encoding

    如果请求具有Transfer-Encoding: chunked标头,则服务器必须等待最后一个块。 这至少在两个地方有所描述:

    1. Section 3.6.1的BNF中。 请注意Chunked-Body必须last-chunk
    2. Section 19.4.6的伪代码中。 观察到没有&#34;向客户发送响应&#34;或者循环内部的任何类似的东西(在整个伪代码中,真的)。
    3. 简而言之,不允许交错。 分块传输编码不会引入交错,因此不会改变HTTP的工作方式。

      PHP

      因为服务器必须等待请求,所以在请求完成之前不会调用PHP。 因此,当您发送3秒延迟的数据块时,您的PHP脚本甚至还没有运行。

      对于PHP配置项enable_post_data_rendering,它不存在。 离它最近的是enable_post_data_reading, 这只是意味着不会解析请求体,因此$ _FILES和$ _POST将为空。 这是出于效率的原因:没有时间花在解析请求体上,也没有用于保存$ _FILES和$ _POST值的内存。 它与POST身体缓冲无关。

      如果您有任何事情仍然不清楚,请告诉我。

      更新

      这是我自己实验的输出,事件之间间隔为3秒,插槽超时为15秒。 时间戳可用于确定哪些事件链接在一起。

      观察在发送最后一个块之前,服务器的读取总是超时。 还要在发送最后一个块时观察时间戳 13:43:03 ,这也是调用PHP时的时间戳。 它显示服务器在调用PHP之前等待最后一个块。

      client 13:40:54 opening socket... opened
      client 13:40:57 sending request... 130 bytes sent
      client 13:41:00 reading from server...
      client 13:41:15 timed out
      client 13:41:18 sending chunk 0... 14 bytes sent
      client 13:41:21 reading from server...
      client 13:41:36 timed out
      client 13:41:39 sending chunk 1... 14 bytes sent
      client 13:41:42 reading from server...
      client 13:41:57 timed out
      client 13:42:00 sending chunk 2... 14 bytes sent
      client 13:42:03 reading from server...
      client 13:42:18 timed out
      client 13:42:21 sending chunk 3... 14 bytes sent
      client 13:42:24 reading from server...
      client 13:42:39 timed out
      client 13:42:42 sending chunk 4... 14 bytes sent
      client 13:42:45 reading from server...
      client 13:43:00 timed out
      client 13:43:03 sending last chunk... 5 bytes sent
      client 13:43:06 reading from server...
      client 13:43:06 279 bytes read
      client 13:43:06 ---------- start of response
      HTTP/1.1 200 OK
      Host: localhost
      Connection: close
      X-Powered-By: PHP/7.0.12
      Transfer-Encoding: chunked
      Content-Type: application/octet-stream
      
      20
      server 2017-06-16 13:43:03 start
      2d
      13:41:18
      13:41:39
      13:42:00
      13:42:21
      13:42:42
      
      1e
      server 2017-06-16 13:43:03 end
      0
      
      client 13:43:06 ---------- end of response
      client 13:43:06 done
      

      这是server.php

      <?php
      while(@ob_end_flush());
      header("Transfer-Encoding: chunked");
      header("Content-Type: application/octet-stream");
      
      echo chunk("server ".gmdate("Y-m-d H:i:s ")."start");
      
      if($f = fopen("php://input", "r")){
          while($s = fread($f, 1024)){
              echo chunk($s);
          }
          fclose($f);
      }
      
      echo chunk("server ".gmdate("Y-m-d H:i:s ")."end");
      echo chunk("");
      
      function chunk($s){
          return dechex(strlen($s))."\r\n".$s."\r\n";
      }
      

      这是client.php

      <?php
      out("opening socket... ");
      if($socket = fsockopen("localhost", 80, $errno, $error)){
          echo "opened\n";
      
          //set socket timeout to 15 seconds
          stream_set_timeout($socket, 15);
          sleep(3);
      
          out("sending request... ");
          $n = fwrite($socket, "POST http://localhost/server.php HTTP/1.1\r\n"
              ."Host: localhost\r\n"
              ."Transfer-Encoding: chunked\r\n"
              ."Content-Type: application/octet-stream\r\n"
              ."\r\n"
          );
          echo "$n bytes sent\n";
          sleep(3);
      
          readFromServer($socket);
          sleep(3);
      
          for($i=0; $i<5; $i++){
              out("sending chunk {$i}... ");
              $n = fwrite($socket, chunk(gmdate("H:i:s\n")));
              echo "$n bytes sent\n";
              sleep(3);
              readFromServer($socket);
              sleep(3);
          }
          out("sending last chunk... ");
          $n = fwrite($socket, chunk(""));
          echo "$n bytes sent\n";
          sleep(3);
      
          readFromServer($socket);
          fclose($socket);
      }else{
          echo "error\n";
      }
      out("done\n");
      
      function out($s){
          echo "client ".gmdate("H:i:s ").$s;
      }
      
      function chunk($s){
          return dechex(strlen($s))."\r\n".$s."\r\n";
      }
      
      function readFromServer($socket){
          out("reading from server... \n");
          $response = fread($socket, 1024);
          $info = stream_get_meta_data($socket);
          if($info['timed_out']){
              out("timed out\n");
          }else{
              out(strlen($response)." bytes read\n");
              if($response){
                  out("---------- start of response\n");
                  echo $response;
                  out("---------- end of response\n");
              }
          }
      }