通过 PHP 或 Apache 中断从服务器端上传的 HTTP 文件
Posted
技术标签:
【中文标题】通过 PHP 或 Apache 中断从服务器端上传的 HTTP 文件【英文标题】:Break HTTP file uploading from server side by PHP or Apache 【发布时间】:2013-04-16 14:01:16 【问题描述】:当上传大文件 (>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;
结果是......失败再次。
所以请帮助我找到解决此问题的正确路径或告诉我没有办法。
相关问题:
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?
【问题讨论】:
您好!不知道能不能用,看一下fastcgi_finish_request()
函数吧。
它不起作用。我确信这个问题不能用纯 PHP 解决。
你试过github.com/felixge/node-formidable吗?
没有php上传文件限制吗? ***.com/questions/2184513/… 还有***.com/questions/4947107/…
【参考方案1】:
首先,you can try this code yourself using the GitHub repo I created for this。只需克隆存储库并运行node header
。
(剧透,如果您正在阅读本文,并且在时间压力下要完成一些工作并且没有心情学习(:(),最后有一个更简单的解决方案)
总体思路
这是一个很好的问题。您所要求的是非常可能并且不需要客户端,只需要更深入地了解 HTTP 协议的工作原理,同时展示 node.js 是如何发挥作用的 :)
如果我们深入到底层TCP protocol 并针对这种特定情况自行处理 HTTP 请求,这将变得容易。 Node.js 让您可以使用内置的 net module 轻松完成此操作。
HTTP 协议
首先,让我们看看 HTTP 请求是如何工作的。
An HTTP request 由一个标题部分组成,其一般格式为键:值对,由 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 服务器。
实现细节
这个解决方案是最简单的。这只是一个建议。
这是工作流程:
我们需要 node.js 中的 net
模块,它允许我们在 node.js 中创建 tcp 服务器
使用net
模块创建一个TCP 服务器,该模块将监听数据:var tcpServer = net.createServer(function (socket) ...
。不要忘记告诉它监听正确的端口
socket.on("data",function(data)
,每当数据包到达时都会触发。
从 'data' 事件中读取传递的缓冲区的数据,并将其存储在变量中
检查双 CRLF,这确保请求 HEADER 部分已结束 according to the HTTP protocol
假设验证是一个标头(用您的话来说是标记),在解析 只是标头 后检查它,(也就是说,我们得到了双 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);
这是基于 nodejs http
模块中的 request
句柄实际上在标头发送后(但未执行其他任何操作)默认情况下挂钩的事实。 (this in the server module , this in the parser module)
用户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);
您可以看到示例输入/输出here。这将要求您使用适当的 Expect:
标头触发请求。
【讨论】:
是的,我们需要深入研究 TCP 级别并使用socket.end()
切断 SOCKET(和 HTTP)连接。很好的解决方案!非常感谢。
这是一个相关问题,激发了我和@igorw ***.com/questions/16350698/…之间的建设性辩论【参考方案2】:
使用 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>
【讨论】:
【参考方案3】:听起来您正在尝试对上传进行流式传输,并且需要在处理之前进行验证: 这有帮助吗? 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
【讨论】:
我尝试过使用 node.js 但失败了。您能否提供一个可以解决我的问题的演示代码?谢谢! 是的。 Node.js 可以在文件上传过程之前或期间解析表单值。但 Node.js 可能不会中断上传。【参考方案4】:我建议你使用一些客户端插件来上传文件。你可以使用
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');
【讨论】:
感谢您的建议。但我的目标是找到一个PURE 服务器端解决方案,它应该不 取决于任何客户端环境。 PS:我的目标不是检查文件大小,而是检查安全令牌。【参考方案5】:以前的版本有些模糊。所以我重写了代码来展示路由处理和中间件之间的区别。每个请求都会执行中间件。它们按照给定的顺序执行。 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 请求。希望这会有所帮助。
【讨论】:
我正在寻找的解决方案是在我们知道令牌不正确时尝试中断上传过程。是的,我知道服务器可以忽略它收到的数据。但是带宽被浪费了,DOS攻击是可能的。 在这种情况下,您可以使用req.destroy();
而不是req.emit('end'); res.end('Invalid');
,这将立即关闭连接,不会向服务器发送任何内容。但是如果你使用它,你就不能为上传请求发送任何响应。
是的。 “req.destroy()
”是我真正需要的。但是 node.js 的文档中没有这个 API。你说的是哪个中间件?
您可以在没有中间件的情况下使用req.destroy()
。 express.bodyParser()
是处理文件上传的中间件。在我给出的示例中,我在bodyParser
之前使用了中间件app.use(function(req,res,next)...);
,在上传实际发生之前终止请求。
正如你所说:“另一方面,上传处理程序不能中断上传,因为它已经由 express.bodyParser() 处理。”它可能有效,但不适用于我的情况。【参考方案6】:
绕过 PHP 后处理的一种方法是通过 PHP-CLI 路由请求。创建以下 CGI 脚本并尝试将大文件上传到其中。 Web 服务器应该通过终止连接来响应。如果是这样,那么只需打开一个内部套接字连接并将数据发送到实际位置——当然,前提是满足条件。
#!/usr/bin/php
<?php
echo "Status: 500 Internal Server Error\r\n";
echo "\r\n";
die();
?>
【讨论】:
【参考方案7】:您为什么不只使用 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
【讨论】:
我认为这个解决方案仍然是一个“客户端”解决方案。它有效,但不是我问题的答案。也许对我来说没有这样一个纯粹的服务器端解决方案。不管怎样,谢谢。以上是关于通过 PHP 或 Apache 中断从服务器端上传的 HTTP 文件的主要内容,如果未能解决你的问题,请参考以下文章