通过PHP或Apache中断服务器端的HTTP文件上传

Ali*_*lix 11 php mod-rewrite file-upload http node.js

将大文件(> 100M)上传到服务器时,PHP始终首先从浏览器接受整个数据POST.我们无法注入上传过程.

例如,在我的PHP代码中将token整个数据发送到服务器之前检查" " 的值是不可能的:

<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>
Run Code Online (Sandbox Code Playgroud)

所以我试着这样使用mod_rewrite:

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

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';
}
Run Code Online (Sandbox Code Playgroud)

但是......它又失败.mod_rewrite看起来在这种情况下工作太晚了.数据仍然完全转移.

然后我尝试了Node.js,像这样(代码剪辑):

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;
    }
Run Code Online (Sandbox Code Playgroud)

结果是... 再次失败.

所以请帮我找到解决这个问题的正确途径,或者告诉我没办法.

相关问题:

在POST请求完成之前,PHP(使用Apache或Nginx)可以检查HTTP标头吗?

有人可以告诉我如何让这个脚本在开始上传过程之前检查密码,而不是在文件上传之后?

Ben*_*aum 21

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

(剧透,如果你正在读这篇文章,并且在时间紧迫的情况下得到一些工作但没有心情学习(:(),最后有一个更简单的解决方案)

一般的想法

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

如果我们深入到底层TCP协议的一个级别并为这个特定情况自己处理HTTP请求,这可以变得容易.Node.js允许您使用内置的网络模块轻松完成此操作.

HTTP协议

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

HTTP请求包含一个标题部分,其格式为key:value对,由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
Run Code Online (Sandbox Code Playgroud)

"空行"前面的顶部是标题部分,底部是请求的主体.你的请求在正文部分看起来会有点不同,因为它是用编码的,multipart/form-data但标题将保持相似让我们探讨这是如何适用于我们的.

nodejs中的TCP

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

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

实施细节

这个解决方案就像它的骨架一样.这只是一个建议.

这是工作流程:

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

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

    • 在该回调中,监听数据事件socket.on("data",function(data){,这将在数据包到达时触发.
    • 从'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;
};
Run Code Online (Sandbox Code Playgroud)

一种在数据事件中获取的缓冲区中检查双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 };
};
Run Code Online (Sandbox Code Playgroud)

而这个小实用方法:

function isContinuation(str) {
    return str.charAt(0) === " " || str.charAt(0) === "\t";
}
Run Code Online (Sandbox Code Playgroud)

履行

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
Run Code Online (Sandbox Code Playgroud)

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

好吧,我撒谎,有一种更简单的方法!

但那有什么好玩的?如果你最初跳过这里,你将无法学习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);
Run Code Online (Sandbox Code Playgroud)

这是基于requestnodejs http模块中的句柄在发送标头之后实际挂钩的事实(但是没有执行任何其他操作)默认情况下.(这在服务器模块中,这在解析器模块中)

用户igorw建议使用100 Continue标题更清晰的解决方案,假设您定位的浏览器支持它.100 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);
Run Code Online (Sandbox Code Playgroud)

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