Подтвердить что ты не робот

Перерыв загрузки HTTP файла с сервера с помощью PHP или Apache

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

Например, проверьте значение "token" до того, как вся передача данных на сервер будет IMPOSSIBLE в моем 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]

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, как это (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?

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

4b9b3361

Ответ 1

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

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

Общая идея

Это отличный вопрос. То, что вы просите, - это очень возможно и без клиентов, просто более глубокое понимание того, как работает протокол HTTP, показывая, как node.js rock:)

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

TCP в nodejs

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

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

Сведения о реализации

Это решение как голые кости по мере его появления. Это всего лишь предложение.

Вот рабочий поток:

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

  • Создайте TCP-сервер, используя модуль net, который будет прослушивать данные: 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 в модуле nodejs 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);

Здесь вы можете увидеть пример ввода/вывода . Это потребовало бы, чтобы ваш запрос запускался с соответствующим заголовком Expect:.

Ответ 2

Использовать javascript. Отправьте предварительную форму через ajax, когда пользователь нажимает кнопку submit, ждет ответа 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>

Ответ 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');
                    }

            }
        }

Ответ 5

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

Ответ 6

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

#!/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