Управляемый сайт

читайте также по теме: Вывод времени непрерывной работы Windows XP (uptime) через web-интерфейс.

На примере HTTP-сервиса Apache 1.3.x и интерпретатора PHP рассмотрены два метода, обеспечивающие управление загрузкой файлов и структурой сайта. В основе этих методов лежит способ, основанный на прямой обработке запроса к HTTP-серверу (анализ URI).

Содержание:

Управляемая загрузка файлов

Для того, чтобы контролировать скачивание файлов с HTTP-сервера средствами CGI можно применять несколько способов:

При явном перенаправлении на странице помещается ссылка вида:

<a href="download.cgi?file=foofile">foofile</a>

При этом подразумевается, что CGI-метод «download.cgi» разберёт строку запроса формата GET, выделит из парамтера «file» имя скачиваемого файла и осуществит перенаправление на реальное размещение файла «foofile» с помощью поля «Location» в HTTP-ответе:

header ( 'Location: real.foofile.path' );
die ();

При этом появляется возможность только подсчитать примерное количество попыток загрузки файла. Дело в том, что подобным образом невозможно определить финал загрузки (скачал ли клиент файл до конца или отказался) и невозможно пресечь прямую загрузку файла в обход публикуемой ссылки. Поскольку идёт явное перенаправление, навигатор клиента переходит на новую ссылку «real.foofile.path» и скачивает оттуда файл напрямую. Ничто не мешает пользователю в следующий раз зайти по прямому адресу, который выдал навигатор.

Второй метод более сложный и более гибкий. Суть его заключается в том, что реальное размещение файла маскируется в теневой папке, а в публичной папке ничего не размещается. При этом CGI-метод «download.cgi» объявляется обработчиком ошибки «404 HTTP_NOT_FOUND» (файл не найден) в публичной папке, и на него возлагается ответственность за траспорт содержимого файла клиенту. В этом случае «download.cgi» должен распознать имя реального файла по адресному запросу клиента и самостоятельно передать содержимое «foofile» клиенту. CGI-метод не просто получает возможность определить конец загрузки, он становится оператором соединения.

Оператор соединения может фильтровать обращения к файлу:

Для этого пригодятся переменные HTTP-сервера:

Чтобы разобрать адресный запрос достаточно нескольких функций parse_url и pathinfo:

$URI  = parse_url ( $_SERVER['REQUEST_URI'] );
/*
$URI['scheme']     - протокол (например, "http")
$URI['host']       - имя узла
$URI['port']       - номер порта соединения
$URI['user']       - имя пользователя при открытом авторизованном подключении
$URI['pass']       - пароль пользователя при открытом авторизованном подключении
$URI['path']       - полный путь к запрашиваемому файлу
$URI['query']      - GET-запрос (после знака "?" в адресе)
$URI['fragment']   - имя фрагмента в документе (после знака "#" в адресе)
*/
$Path = pathinfo  ( $URI['path'] );
/*
$Path['dirname']   - путь к файлу (без имени)
$Path['basename']  - имя файла (с расширением
$Path['extension'] - расширение (без точки)
*/

«Правильный» ответ клиенту

Так как оператор соединения по сути получает управление при возникновении ошибки «404 HTTP_NOT_FOUND», то для корректного установления соединения необходимо сформировать правильный ответ клиенту. В противном случае навигатор клиента получит в HTTP-заголовке ответа «Status: 404 Not Found», и он будет иметь полное право считать, что получил не содержимое запрашиваемого файла, а только текст, поясняющий ошибку. При этом пользователь увидит в окне навигатора часть запрашиваемого файла в виде текста, что вряд ли его обрадует.

Поэтому надо обязательно сообщить, что на самом деле никакой ошибки нет, и файл на месте. Для этого надо сформировать подходящий HTTP-заголовок ответа.

// Краткий перечень mime-типов
$foofileCType = 'application/octet-stream';
$CTypes = array (
  'pdf' => 'application/pdf',
  'exe' => 'application/octet-stream',
  'zip' => 'application/zip',
  'doc' => 'application/msword',
  'xls' => 'application/vnd.ms-excel',
  'ppt' => 'application/vnd.ms-powerpoint',
  'gif' => 'image/gif',
  'png' => 'image/png',
  'jpe' => 'jpeg',
  'jpg' => 'image/jpg',
);
 
if ( isset ( $CTypes[$Path['extension']] )
  $foofileCType == $CTypes[$Path['extension']];
 
$foofile = $Path['basename'];        // запрашиваемое имя
$realFooFilePath = ...               // реальный путь к файлу
header ( 'HTTP/1.1 200 OK', true, 200 );
header ( 'Status: 200 OK' );
header ( 'Pragma: ' );
header ( 'Cache-control: must-revalidate, post-check=0, pre-check=0' );
header ( 'Content-length: ' . fileSize ( $realFooFilePath ) );
header ( 'Last-Modified: ' . gmdate ( 'r', fileatime ( $realFooFilePath ) ) . ' GMT');
header ( 'Content-disposition: attachment; filename="' . $foofile . '"' );
header ( 'Content-type: ' . $foofileCType );
header ( 'Content-transfer-encoding: binary');

Контроль скорости загрузки файлов

Самый примитивный, но не менее эффективный способ контроля скорости передачи заключается в порционной выдаче содержимого файла, чередующейся с паузой. «Скважность» порций определяет суммарную пропускную способность соединения. Например, надо обеспечить скорость соединения N Кбайт/с. Оператор соединения читает из «foofile» N Кбайт данных, пишет их в поток вывода и ждёт 1 секунду. При этом итоговая средняя скорость будет чуть меньше N Кбайт/с.

@set_time_limit ( 600 );                        // максимальное процессорное время на выполнение сценария (с)
$SpeedLimitBlockSize = N << 10;                 // N - желаемая скорость передачи (Кбайт/с)
$OK = false;
if ( $fp = fopen ( $realFooFilePath, 'rb' ) )   // 'rb' - обязательно для бинарных файлов
{
  while ( !feof ( $fp ) )
  {
    $buffer = fread ( $fp, $SpeedLimitBlockSize );
    print $buffer;
    flush ();
    sleep ( 1 );
  }
  flush ();
  $OK = feof ( $fp );
  fclose ( $fp );
}

Стоит отметить, что HTTP-сервис Apache версии 2.x.x полностью буферизирует вывод, поэтому предложенный метод контроля скорости работать не будет. Сервис будет ждать, пока «download.cgi» не выдаст полностью всё содержимое и только потом запишет его в выходной поток.

Докачка файлов

Считается хорошим тоном, если сервис позволяет докачивать файлы в случае оборванных соединений. Поэтому при реализации оператора соединения необходимо предусмотреть вариант восстановления загрузки с заданного места. С точки зрения протокола HTTP происходит следующее:

if ( isset ( $_SERVER['HTTP_RANGE'] ) )
{
  if ( preg_match ( '/bytes=([0-9]+)-/i', $_SERVER['HTTP_RANGE'], $range ) && isset ( $range[1] ) )
  {
    $rangePosition = intval ( $range[1] );
    $rangeResponse = $rangePosition . '-' . fileSize ( $realFooFilePath ) - 1;
    header ( 'HTTP/1.1 206  Partial content', true , 206);
    header ( 'Status: 206 Partial content' );
    header ( 'Content-range: bytes ' . $rangeResponse . '/' . fileSize ( $realFooFilePath ) );
    header ( 'Last-Modified: ' . gmdate ('r', fileatime ( $realFooFilePath ) ) . ' GMT');
    header ( 'Content-disposition: attachment; filename="' . $foofile . '"' );
    header ( 'Content-type: ' . $foofileCType );
    header ( 'Content-transfer-encoding: binary');
  }
}

После этого достаточно встать на позицию «$rangePosition» и выдать в поток содержимое файла.

Управляемая структура сайта

Аналогично рассмотренному выше подходу, названному «оператор соединения», реализуется управление структурой виртуальных документов и виртуальных папок. В отличие от традиционной организации струкутры сайта, когда в домашней директории создаются соответствующие папки и файлы (пути к которым транслируются навигатору клиента относительно домашней директории), виртуальные папки и файлы могут вовсе не существовать. Специализированный обработчик ошибки «404 HTTP_NOT_FOUND» (назовём его «диспетчер запросов») анализирует строку запроса и формирует соответствующий ответ. При этом навигатор клиента работает в штатном режиме и пользователь не замечает разницы между реальными папками и файлами HTTP-сервера и виртуальными.

Перенаправление запросов

Диспетчер запросов должен иметь информацию о виртуальной структуре, для этого вполне подойдёт ассоциативный массив PHP, в котором в качестве ключей будут выступать вирутальный пути к вирутальным документам. Например, пусть необходимо сформировать следующую структуру:

Во-первых, необходимо выделить путь из строки запроса:

$URI     = parse_url   ( $_SERVER['REQUEST_URI'] );
$URIpath = str_replace ( '\\', '/', $URI['path'] );

Затем, с помощью ассоциативного массива виртуальной структуры определить правильность запроса и сформировать ответ.

$ShadowPath = 'shadow/';
 
$Structure = array (
  '/' => 'general.inc',              // значение --- теневое размещение файла
  '/index.htm' => 'general.inc',
  '/products/' => 'products.inc',
  '/products/index.htm' => 'products.inc',
  '/products/product1/' => 'product.1.inc',
  '/products/product1/index.htm' => 'product.1.inc',
  '/products/product1/price.htm' => 'product.1.price.inc',
  '/products/product2/' =>  'product.2.inc',
  '/products/product2/index.htm' => 'product.2.inc',
  '/products/product2/support.htm' => 'product2.support.2.inc',
  '/about/' => 'about.inc',
  '/about/index.htm' =>  'about.inc',
);
 
if ( isset ( $Structure[$URIpath] ) )
{
  $FileName = $ShadowPath . $Structure[$URIpath];
  $FileTime = filemtime ( $FileName );
 
  if (
       isset     ( $headers['If-Modified-Since'] ) &&
       strtotime ( $headers['If-Modified-Since'] ) == $FileTime
     )
  {
    // Файл не менялся
    header  ( 'Last-Modified: ' . gmdate ( 'r', $FileTime) . ' GMT', true, 304 );
  }
  else
  {
    // Файл изменился со времени последнего обращения
    header  ( 'Last-Modified: ' . gmdate ( 'r', $FileTime) . ' GMT', true, 200 );
    include ( $FileName );
  }
  flush ();
}
else
{
  // Вывод диагностической страницы с ошибкой или иная обработка.
  // ...
}

Обработка GET и POST-запросов

К сожалению, так получается, что обработчик ошибки «404 HTTP_NOT_FOUND» не получает значения переменных $_GET и $_POST, поэтому теряется возможность использования интерактивных форм на сайте. Обходное решение, хоть и не совсем изящное, но существует. Предлагается во всех формах в качестве обработчика POST запросов явно указать URI диспетчера. При этом надо сделать копию данных POST в окружение $_SESSION и перенаправить навигатор пользователя на адрес, с которого были посланы данные POST, но уже с установленным окружением.

session_start ();
$_SESSION['name'] = 'foo.site';
 
if ( !isset ( $_SESSION['initiated'] ) )
{
   session_regenerate_id ();
   $_SESSION ['initiated'] = true;
}
 
if( $_SERVER['REQUEST_METHOD'] == 'POST' )
{
  $_SESSION['_POST'] = $_POST;                         // сохраняем копию данных POST в окружение SESSION
  header ( 'Location: ' . $_SERVER['HTTP_REFERER'] );  // перенаправляем обратно, чтобы клиент не увидел изменений в адресной строке
  die ();
}
else
{
  if ( is_array( $_SESSION['_POST'] ) )
  {
    $_POST = $_SESSION['_POST'];                       // обратно восстанавливаем данные POST
    unset ( $_SESSION['_POST'] );
  }
}

Для восстановления данных GET-запроса подобный метод был бы рассточительным, поэтому данные GET можно просто-напросто прочитать вручную.

function MakeGetArray ( $query )
{
  function ExtractParam ( & $query, $Start, $End )
  {
    global $_GET;
    $temp = substr ( $query, $Start, $End );
    if ( 1 < ( $EqPos = strpos ( $temp, '=', 0 ) ) )
      $_GET [substr ( $temp, 0, $EqPos ) ] = substr ( $temp, $EqPos + 1, strlen ( $temp ) - $EqPos );
  }
 
  $Start = 0;
  $AmpPos = strpos ( $query, '&', $Start );
  if ( 2 < $AmpPos )
  {
    ExtractParam ( $query, $Start, $AmpPos );
    $Start = $AmpPos + 1;
  }
  ExtractParam ( $query, $Start, strlen ( $query ) );
}
// заполняем массив $_GET
MakeGetArray ( $URI['query'] )

30 мая 2005—30 мая 2005
Максим Проскурня
1997–2024 Axofiber, axofiber.info