11

When uploading big file (>100M) to server, PHP always accept entire data POST from browser first. We cannot inject into the process of uploading.

For example, check the value of "token" before entire data send to server is IMPOSSIBLE in my PHP code:

<form enctype="multipart/form-data" action="upload.php?token=XXXXXX" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000" />
    Send this file: <input name="userfile" type="file" />
    <input type="submit" value="Send File" />
</form>

So I've try to use mod_rewrite like this:

RewriteEngine On
RewriteMap mymap prg:/tmp/map.php
RewriteCond %{QUERY_STRING} ^token=(.*)$ [NC]
RewriteRule ^/upload/fake.php$ ${mymap:%1} [L]

map.php

#!/usr/bin/php
<?php
define("REAL_TARGET", "/upload/real.php\n");
define("FORBIDDEN", "/upload/forbidden.html\n");

$handle = fopen ("php://stdin","r");
while($token = trim(fgets($handle))) {
file_put_contents("/tmp/map.log", $token."\n", FILE_APPEND);
    if (check_token($token)) {
        echo REAL_TARGET;
    } else {
        echo FORBIDDEN;
    }
}

function check_token ($token) {//do your own security check
    return substr($token,0,4) === 'alix';
}

But ... It fails again. mod_rewrite looks working too late in this situation. Data still transfer entirely.

Then I tried Node.js, like this (code snip):

var stream = new multipart.Stream(req);
stream.addListener('part', function(part) {
    sys.print(req.uri.params.token+"\n");
    if (req.uri.params.token != "xxxx") {//check token
      res.sendHeader(200, {'Content-Type': 'text/plain'});
      res.sendBody('Incorrect token!');
      res.finish();
      sys.puts("\n=> Block");
      return false;
    }

Result is ... fail again.

So please help me to find the correct path to resolve this issue or tell me there is no way.

Related questions:

Can PHP (with Apache or Nginx) check HTTP header before POST request finished?

Can some tell me how to make this script check for the password before it starts the upload process instead of after the file is uploaded?

4

7 回答 7

23

首先,您可以使用我为此创建的 GitHub 存储库自己尝试此代码。只需克隆存储库并运行node header.

(剧透,如果你正在阅读这篇文章,并且在时间压力下要完成一些工作并且没有心情学习(:(),最后有一个更简单的解决方案)

总体思路

这是一个很好的问题。您所要求的非常有可能,并且不需要客户端,只需更深入地了解 HTTP 协议的工作原理,同时展示 node.js 是如何运作的 :)

如果我们更深入地了解底层TCP 协议并针对这种特定情况自己处理 HTTP 请求,这将变得容易。Node.js 让您可以使用内置的net 模块轻松完成此操作。

HTTP 协议

首先,让我们看看 HTTP 请求是如何工作的。

HTTP 请求包含一个头部部分,其一般格式为键:值对,由 CRLF ( \r\n) 分隔。我们知道,当我们到达双 CRLF(即\r\n\r\n)时,标题部分结束。

典型的 HTTP GET 请求可能如下所示:

GET /resource HTTP/1.1  
Cache-Control: no-cache  
User-Agent: Mozilla/5.0 

Hello=World&stuff=other

“空行”之前的顶部是标题部分,底部是请求的正文。您的请求在正文部分看起来会有所不同,因为它是用编码的,multipart/form-data但标头将保持相似让我们来探索这如何适用于我们。

nodejs中的TCP

我们可以在 TCP 中侦听原始请求并读取我们得到的数据包,直到我们读取我们谈到的双 crlf。然后我们将检查我们已经拥有的短标题部分以进行我们需要的任何验证。在我们这样做之后,如果验证没有通过(例如通过简单地结束 TCP 连接),我们可以结束请求,或者通过它。这允许我们不接收或读取请求正文,而只是接收或读取更小的标头。

将其嵌入到现有应用程序中的一种简单方法是将来自应用程序的请求代理到特定用例的实际 HTTP 服务器。

实施细节

这个解决方案是最简单的。这只是一个建议。

这是工作流程:

  1. 我们需要 node.js 中的net模块,它允许我们在 node.js 中创建 tcp 服务器

  2. net使用将监听数据 的模块创建一个 TCP 服务器: var tcpServer = net.createServer(function (socket) {.... 不要忘记告诉它监听正确的端口

  • 在该回调中,监听数据事件socket.on("data",function(data){,只要数据包到达就会触发。
  • 从“数据”事件中读取传递缓冲区的数据,并将其存储在变量中
  • 检查双 CRLF,这确保请求 HEADER 部分已根据 HTTP 协议结束
  • 假设验证是一个标头(用您的话来说是标记),在仅解析标头后检查它,(也就是说,我们得到了双 CRLF)。这在检查内容长度标头时也有效。
  • 如果您注意到标头没有签出,请调用socket.end()它将关闭连接。

这是我们将使用的一些东西

读取标题的方法:

function readHeaders(headers) {
    var parsedHeaders = {};
    var previous = "";    
    headers.forEach(function (val) {
        // check if the next line is actually continuing a header from previous line
        if (isContinuation(val)) {
            if (previous !== "") {
                parsedHeaders[previous] += decodeURIComponent(val.trimLeft());
                return;
            } else {
                throw new Exception("continuation, but no previous header");
            }
        }

        // parse a header that looks like : "name: SP value".
        var index = val.indexOf(":");

        if (index === -1) {
            throw new Exception("bad header structure: ");
        }

        var head = val.substr(0, index).toLowerCase();
        var value = val.substr(index + 1).trimLeft();

        previous = head;
        if (value !== "") {
            parsedHeaders[head] = decodeURIComponent(value);
        } else {
            parsedHeaders[head] = null;
        }
    });
    return parsedHeaders;
};

一种在数据事件中检查缓冲区中的双 CRLF 的方法,如果它存在于对象中,则返回其位置:

function checkForCRLF(data) {
    if (!Buffer.isBuffer(data)) {
        data = new Buffer(data,"utf-8");
    }
    for (var i = 0; i < data.length - 1; i++) {
        if (data[i] === 13) { //\r
            if (data[i + 1] === 10) { //\n
                if (i + 3 < data.length && data[i + 2] === 13 && data[i + 3] === 10) {
                    return { loc: i, after: i + 4 };
                }
            }
        } else if (data[i] === 10) { //\n

            if (data[i + 1] === 10) { //\n
                return { loc: i, after: i + 2 };
            }
        }
    }    
    return { loc: -1, after: -1337 };
};

还有这个小实用方法:

function isContinuation(str) {
    return str.charAt(0) === " " || str.charAt(0) === "\t";
}

执行

var net = require("net"); // To use the node net module for TCP server. Node has equivalent modules for secure communication if you'd like to use HTTPS

//Create the server
var server = net.createServer(function(socket){ // Create a TCP server
    var req = []; //buffers so far, to save the data in case the headers don't arrive in a single packet
    socket.on("data",function(data){
        req.push(data); // add the new buffer
        var check = checkForCRLF(data);
        if(check.loc !== -1){ // This means we got to the end of the headers!
            var dataUpToHeaders= req.map(function(x){
                return x.toString();//get buffer strings
            }).join("");
            //get data up to /r/n
            dataUpToHeaders = dataUpToHeaders.substring(0,check.after);
            //split by line
            var headerList = dataUpToHeaders.trim().split("\r\n");
            headerList.shift() ;// remove the request line itself, eg GET / HTTP1.1
            console.log("Got headers!");
            //Read the headers
            var headerObject = readHeaders(headerList);
            //Get the header with your token
            console.log(headerObject["your-header-name"]);

            // Now perform all checks you need for it
            /*
            if(!yourHeaderValueValid){
                socket.end();
            }else{
                         //continue reading request body, and pass control to whatever logic you want!
            }
            */


        }
    });
}).listen(8080); // listen to port 8080 for the sake of the example

如果您有任何问题随时问 :)

好吧,我撒谎了,还有更简单的方法!

但这有什么乐趣呢?如果您最初跳过此处,您将不会了解 HTTP 的工作原理 :)

Node.js 有一个内置http模块。由于 node.js 中的请求本质上是分块的,尤其是长请求,因此您可以在不深入了解协议的情况下实现相同的事情。

这次,让我们使用该http模块来创建一个http服务器

server = http.createServer( function(req, res) { //create an HTTP server
    // The parameters are request/response objects
    // check if method is post, and the headers contain your value.
    // The connection was established but the body wasn't sent yet,
    // More information on how this works is in the above solution
    var specialRequest = (req.method == "POST") && req.headers["YourHeader"] === "YourTokenValue";
    if(specialRequest ){ // detect requests for special treatment
      // same as TCP direct solution add chunks
      req.on('data',function(chunkOfBody){
              //handle a chunk of the message body
      });
    }else{
        res.end(); // abort the underlying TCP connection, since the request and response use the same TCP connection this will work
        //req.destroy() // destroy the request in a non-clean matter, probably not what you want.
    }
}).listen(8080);

这是基于默认情况下request,nodejshttp模块中的句柄在发送标头后(但未执行其他任何操作)实际上挂钩的事实。(服务器模块的这个,解析器模块中的这个)

假设您的目标浏览器支持它,用户igorw建议使用标题更简洁的解决方案。100 Continue100 Continue 是一个状态代码,旨在完全按照您的尝试:

100(继续)状态(参见第 10.1.1 节)的目的是允许正在发送带有请求正文的请求消息的客户端确定源服务器是否愿意接受请求(基于请求标头)在客户端发送请求正文之前。在某些情况下,如果服务器在不查看正文的情况下拒绝消息,则客户端发送正文可能不合适或效率极低。

这里是 :

var http = require('http');
 
function handle(req, rep) {
    req.pipe(process.stdout); // pipe the request to the output stream for further handling
    req.on('end', function () {
        rep.end();
        console.log('');
    });
}
 
var server = new http.Server();
 
server.on('checkContinue', function (req, rep) {
    if (!req.headers['x-foo']) {
        console.log('did not have foo');
        rep.writeHead(400);
        rep.end();
        return;
    }
 
    rep.writeContinue();
    handle(req, rep);
});
 
server.listen(8080);

您可以在此处查看示例输入/输出。这将要求您使用适当的Expect:标头触发请求。

于 2013-05-03T00:38:38.300 回答
1

使用 JavaScript。当用户点击提交时,通过 ajax 提交一个 pre-form,等待 ajax 响应,然后当它返回成功与否时,提交实际的表单。您也可以回退到您不想要的方法,这总比没有好。

<script type="text/javascript">
function doAjaxTokenCheck() {
    //do ajax request for tokencheck.php?token=asdlkjflgkjs
    //if token is good return true
    //else return false and display error
}
</script>

<form enctype="multipart/form-data" action="upload.php?token=XXXXXX" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000" />
    Send this file: <input name="userfile" type="file" />
    <input type="submit" value="Send File" onclick="return doAjaxTokenCheck()"/>
</form>
于 2013-05-02T13:31:41.320 回答
0

为什么不只使用 APC 文件上传进度并将进度密钥设置为 APC 文件上传的密钥,这样在这种情况下,表单已提交并且上传进度将首先开始,但在第一次进度检查时,您将验证密钥如果它不正确,你会打断一切:

http://www.johnboy.com/blog/a-useful-php-file-upload-progress-meter http://www.ultramegatech.com/2008/12/creating-upload-progress-bar-php/

这是一种更原生的方法。大致相同,只需将隐藏输入的密钥更改为您的令牌并验证它并在出现错误时中断连接。也许那会更好。 http://php.net/manual/en/session.upload-progress.php

于 2013-05-02T02:23:18.723 回答
0

绕过 PHP 后处理的一种方法是通过 PHP-CLI 路由请求。创建以下 CGI 脚本并尝试将大文件上传到其中。Web 服务器应该通过终止连接来响应。如果是这样,那么只需打开一个内部套接字连接并将数据发送到实际位置——当然,前提是满足条件。

#!/usr/bin/php
<?php

echo "Status: 500 Internal Server Error\r\n";
echo "\r\n";
die();

?>
于 2013-04-27T18:14:30.563 回答
0

我建议您使用一些客户端插件来上传文件。你可以使用

http://www.plupload.com/

或者

https://github.com/blueimp/jQuery-File-Upload/

两个插件都可以在上传前检查文件大小。

如果您想使用自己的脚本,请选中此项。这可能会帮助你

        function readfile()
        {
            var files = document.getElementById("fileForUpload").files;
            var output = [];
            for (var i = 0, f; f = files[i]; i++) 
            {
                    if(f.size < 100000) // Check file size of file
                    {
                        // Your code for upload
                    }
                    else
                    {
                        alert('File size exceeds upload size limit');
                    }

            }
        }
于 2013-04-26T09:30:45.287 回答
0

听起来您正在尝试流式传输上传并需要在处理之前进行验证:这有帮助吗? http://debuggable.com/posts/streaming-file-uploads-with-node-js:4ac094b2-b6c8-4a7f-bd07-28accbdd56cb

http://www.componentix.com/blog/13/file-uploads-using-nodejs-once-again

于 2013-04-23T11:55:48.590 回答
0

以前的版本有些模糊。所以我重写了代码来展示路由处理和中间件之间的区别。每个请求都会执行中间件。它们按照给定的顺序执行。express.bodyParser()是处理文件上传的中间件,你应该跳过不正确的令牌。mymiddleware只需检查令牌并终止无效请求。这必须在express.bodyParser()执行之前完成。

var express = require('express'),
app = express();

app.use(express.logger('dev'));
app.use(mymiddleware);                                 //This will work for you.
app.use(express.bodyParser());                         //You want to avoid this
app.use(express.methodOverride());
app.use(app.router);

app.use(express.static(__dirname+'/public'));
app.listen(8080, "127.0.0.1");

app.post('/upload',uploadhandler);                     //Too late. File already uploaded

function mymiddleware(req,res,next){                   //Middleware
    //console.log(req.method);
    //console.log(req.query.token);
    if (req.method === 'GET')
        next();
    else if (req.method === 'POST' && req.query.token === 'XXXXXX')
        next();
    else
        req.destroy();
}

function uploadhandler(req,res){                       //Route handler
    if (req.query.token === 'XXXXXX')
        res.end('Done');
    else
        req.destroy();
}

uploadhandler另一方面不能中断上传,因为它已经被处理过express.bodyParser()。它只处理 POST 请求。希望这可以帮助。

于 2013-04-26T18:10:51.593 回答