Прервать загрузку HTTP-файла с сервера PHP или Apache

при загрузке большого файла (>100M) на сервер PHP всегда сначала принимает весь пост данных из браузера. Мы не можем вмешиваться в процесс загрузки.

например, проверьте значение"token " перед отправкой всех данных на сервер невозможно в моем PHP-коде:

<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]

карта.в PHP

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

$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, вот так (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;
    }

результат ... не еще раз.

поэтому, пожалуйста, помогите мне найти правильный путь для решения этой проблемы или скажите мне, что нет никакого способа.

вопросы:

может ли PHP (с Apache или Nginx) проверить HTTP-заголовок до завершения запроса POST?

может кто подскажет как сделать этот скрипт проверить пароль до начала процесса загрузки, а не после загрузки файла?

7 ответов


прежде всего,вы можете попробовать этот код самостоятельно, используя репозиторий GitHub, который я создал для этого. Просто клонируйте репозиторий и запустите node header.

(спойлер, если Вы читаете это и находитесь под давлением времени, чтобы заставить что-то работать, а не в настроении учиться (: (), есть более простое решение в конце)

общая идея

это большой вопрос. То, что вы просите, это вполне возможно и нет клиентские необходимо, просто более глубокое понимание того, как работает протокол HTTP, показывая, как узел.Яш камни :)

это можно сделать легко, если мы пойдем на один уровень глубже к основному протокол TCP и обрабатывать HTTP-запросы для этого конкретного случая. Узел.js позволяет сделать это легко, используя встроенный объем модуля.

протокол 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 а заголовок останется similarLet рассмотрим, как это относится к нам.

TCP в nodejs

мы можем слушать необработанный запрос в TCP и читать пакеты, которые мы получаем, пока не прочитаем тот двойной crlf, о котором мы говорили. Затем мы проверим короткий раздел заголовка, который у нас уже есть для любой проверки, которая нам нужна. После этого мы можем либо завершить запрос, если проверка не прошла (например, просто завершение TCP-соединения), или передать его через. Это позволяет нам не получать или читать тело запроса, а только заголовки, которые намного меньше.

один простой способ встроить это в уже существующее приложение-это прокси-запросы от него к фактическому http-серверу для конкретного случая использования.

детали реализации

данное решение как голые кости как получится. Это просто предложение.

здесь рабочий поток:

  1. нам требуется net модуль в узле.js, который позволяет нам создавать tcp-серверы в узле.js

  2. создайте TCP-сервер с помощью net модуль, который будет слушать данные:var tcpServer = net.createServer(function (socket) {... . Не забудьте сказать ему, чтобы слушать правильный порт

    • внутри этого обратного вызова слушайте события данных socket.on("data",function(data){, который будет срабатывать всякий раз, когда приходит пакет.
    • прочитайте данные пройденного буфер из события "data" и сохраните его в переменной
    • проверьте наличие двойного CRLF, это гарантирует, что раздел заголовка запроса закончился согласно протоколу HTTP
    • предполагая, что проверка является заголовком (токен в ваших словах), проверьте его после разбора просто заголовки, (то есть мы получили двойной CRLF). Это также работает при проверке заголовка content-length.
    • если вы заметили, что заголовки не проверьте, позвоните 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:)

узел.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 ручка в nodejs http модуль фактически подключается после отправки заголовков (но больше ничего не было выполнено) по умолчанию. (это в серверном модуле , это в модуле парсера)

пользователей igorw предложил несколько более чистое решение, используя 100 Continue заголовок, предполагающий, что браузеры, на которые вы нацелены, поддерживают его. 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);

вы можете увидеть образец ввода / вывода здесь. Для этого потребуется ваша просьба стрелять с соответствующим .


используйте 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>

похоже, вы пытаетесь передать загрузку и должны проверить перед обработкой: Это поможет? 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


Я предлагаю вам использовать некоторые плагины на стороне клиента для загрузки файлов. Вы могли бы использовать

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

            }
        }

предыдущая версия была несколько расплывчатой. Поэтому я переписал код, чтобы показать разницу между обработкой маршрутов и промежуточным программным обеспечением. Middlewares выполняются для каждого запроса. Они исполняются в указанном порядке. 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. Надеюсь, это поможет.


один из способов обойти обработку сообщений PHP-это маршрутизировать запрос через PHP-CLI. Создайте следующий сценарий CGI и попробуйте загрузить в него большой файл. Веб-сервер должен ответить, убив соединение. Если это так, то это просто вопрос открытия внутреннего сокета и отправки данных в фактическое местоположение-при условии, что условия выполнены, конечно.

#!/usr/bin/php
<?php

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

?>

Почему бы вам просто не использовать прогресс загрузки файла 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