通过PHP或Apache从服务器端上传HTTP文件

在将大文件(> 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> 

所以我尝试使用这个mod_rewrite

 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'; } 

但是…它再次失败。 在这种情况下, 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; } 

结果是… 再次失败。

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

相关问题:

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

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

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

(破坏者,如果你正在阅读这些内容,并且在时间压力下努力工作,而没有学习的心情(:(),最后有一个更简单的解决scheme)

总的想法

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

如果我们深入一层底层的TCP协议并且自己处理这个HTTP请求,这个可以变得简单。 Node.js让你可以使用内置的networking模块轻松完成。

HTTP协议

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

HTTP请求由CRLF( \r\n )分隔的key:value对的一般格式中的header部分组成。 我们知道,当我们达到双重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。 然后,我们将检查我们已经有的短头部分,用于我们需要的任何validation。 在我们这样做之后,如果validation没有通过,我们可以结束请求(例如通过简单地结束TCP连接),或者传递它。 这使我们不能接收或读取请求体,而只是更小的头。

将其embedded已经存在的应用程序中的一种简单方法是将请求从代理请求代理到特定用例的实际HTTP服务器。

实施细节

这个解决scheme就像它的骨头一样。 这只是一个build议。

这里是工作stream程:

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

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

    • 在callback的内部,监听数据事件socket.on("data",function(data){ ,每当数据包到达时都会触发。
    • 从“data”事件中读取传入缓冲区的数据,并将其存储在一个variables中
    • 检查双重CRLF,这确保请求HEADER部分根据HTTP协议结束
    • 假设validation是一个头(在你的文字中的标记)检查后parsing只是头 ,(也就是说,我们得到了双CRLF)。 这也适用于检查内容长度标题。
    • 如果你注意到头文件没有检出,就调用socket.end()来closures连接。

这里有一些我们将要使用的东西

读取标题的方法:

 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模块来创build一个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); 

这是基于事实上,在默认情况下,在发送头文件之后,nodejs http模块中的request句柄实际上会挂起(但没有其他任何操作)。 (这在服务器模块中 , 这在parsing器模块中)

用户igorwbuild议使用100 Continue标题,假设浏览器支持它,一个更清洁的解决scheme。 100继续是一个状态代码,旨在完成你正在尝试的内容:

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); 

您可以在这里看到示例input/输出。 这将要求您的请求使用适当的Expect:标题进行触发。

使用javascript。 当用户点击提交时,通过ajax提交一个预先的表单,等待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> 

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

http://www.plupload.com/

要么

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

上传之前,两个插件都提供了检查文件大小的function。

如果你想使用你自己的脚本,检查这个。 这可能会帮助你

  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'); } } } 

以前的版本有些含糊。 所以我重写了代码,以显示路由处理和中间件之间的区别。 每个请求都执行中间件。 他们按照他们的顺序执行。 express.bodyParser()是处理file upload的中间件,对于不正确的令牌,你应该跳过这个中间件。 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请求。 希望这可以帮助。

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

 #!/usr/bin/php <?php echo "Status: 500 Internal Server Error\r\n"; echo "\r\n"; die(); ?> 

为什么不使用APCfile upload进度,并将进度键设置为APCfile upload的关键字,因此在这种情况下,表单将被提交,上传进度将首先启动,但是在第一次进度检查时,您将validation密钥如果它不正确,你会打断一切:

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

这是一个更原生的做法。 大致相同,只需将隐藏input的密钥更改为令牌,并在发生错误时validation并中断连接。 也许这更好。 http://php.net/manual/en/session.upload-progress.php