Шаг 1 - Что такое CGI ?
CGI - Common Gateway Interface
CGI является стандартом интерфейса, который служит для связи внешней программы с веб-сервером. Программу, которая работает по такому интерфейсу совместно с веб-сервером, принято называть шлюзом, многие больше любят названия скрипт или
CGI-программа.
Сам протокол разработан таким образом, чтобы можно было использовать любой язык программирования, который может работать со стандартными устройствами ввода/вывода.
А это умеет даже сама операционная система, поэтому часто если вам не требуется сложный скрипт, его можно просто сделать в виде командного файла.
В web-сервере Apache такая настройка производится с помощью файла .htaccess в той директории, где содержится этот скрипт. Вот содержание такого файла:
Options ExecCGI
Также Apache позволяет запускать все скрипты имеющие рассширение .cgi, если в файле настроек сервера httpd.cong есть настройка:
AddHandler cgi-script .cgi
Чаще всего, хотя наверно почти всегда, скрипты используются для создания динамических страниц. Связано это с тем, что само содержимое веб-сервера является статическим и не будет меняться просто так, для этого должен приложить руку веб-мастер.
Технология CGI позволяет просто поменять содержимое веб-сервера.
Простым примером может служить скрипт, который при каждом новом обновлении страницы вставляет в нее новую ссылку(банер) или анекдот. Более сложными скриптами являются гостевые книги, чаты, форумы и естественно поисковые сервера или базы данных построенные на технологиях интернета.
Шаг 2 - Передача данных шлюзу.
Передача данных шлюзу осуществляется в следующем формате:
имя=значение&имя1=значение1&...
Здесь "имя" это название параметра, а "значение" его содержимое.
Методов передачи данных в таком формате существует два - GET и POST.
При использовании метода GET данные передаются серверу вместе с URL:
http://.../cgi-bin/test.cgi?имя=значение&имя1=значение1&...
При использовании метода POST данные посылаются внутри самого HTTP запроса.
Так как длина URL ограничена, то методом GET нельзя передать большой объем данных, а метод POST обеспечивает передачу данных не ограниченных по длинне.
Получение данных самим скриптом также различается. При использовании метода GET данные следующие за "?" помещаются в переменную среды QUERY_STRING.
При использовании POST содержимое запроса перенаправляется в стандартный поток ввода, т.е. в stdin.
Чтобы шлюз мог узнать какой метод используется для передачи данных, сервер создает переменную среды REQUEST_METHOD, в которую записывает GET или POST.
В имена и значения параметров при передаче кодируются браузером URL методом, т.е. все символы не принадлежащие к латинскому алфавиту и числам кодируются в виде %HH, где HH - шестнадцатеричное значение кода символа.
Также кодируются все символы , которые нельзя использовать, т.е. !#%^&()=+ и пробел.
Символ "&" используется, как мы уже видели для разделения пар "имя=значение", "=" используется в парах "имя=значение", "%" для кодирования символов, "пробел" кодируется символом "+"(плюс), сам же плюс кодируется через "%", и т.д.
Поэтому при анализе полученных данных требуется их переводить в нормальных вид.
Пример кодировки символов и букв при передаче:
Передается строка: !@#$%^&*()-=_+ абвгд
Скрипт получает : %21@%23%24%25%5E%26*%28%29-%3D_%2B+%E0%E1%E2%E3%E4
Размер передаваемых данных методом POST содержится в переменной окружения CONTENT_LENGTH:
Передается : a=4&b=1
CONTENT_LENGTH = 7
Шаг 3 - Структура CGI программы.
Как и любая программа, программа CGI шлюза должна получить данные, обработать их и вывести результат работы в наглядной форме.
Как получать данные ?
Весь процесс получения данных от веб-сервера можно представить следуюшим образом:
- Получить все необходимые переменные окружения. Наиболее важной на данном этапе является REQUEST_METHOD.
- Получить данные от сервера в зависимости от метода передачи:
Если (REQUEST_METHOD="GET") то
Взять данные из переменной окружения QUERY_STRING
Иначе
Если (REQUEST_METHOD="POST") то
Проанализировать переменную QUERY_STRING
Получить длинну данных из CONTENT_LENGTH
Если (CONTENT_LENGTH>0) то
Считать CONTENT_LENGTH байт из sdtin как данные.
Иначе
Выдать сообщение об ошибке и выйти.
- Декодировать все полученные данные и, если надо, разбить их на пары "имя=значение" в удобную для программы форму.
С анализом переменной REQUEST_METHOD думаю все ясно, а вот ,что значит в методе POST проанализировать QUERY_STRING, наверно, не все ясно.
При передаче методом POST сервер посылает данные через стандартный поток ввода, но это не значит, что пользователь не пользовался URL\'ом для передачи данных.
Примером может служить многопользовательский шлюз, в котором для идентификации пользователя используется URL, а для передачи данных stdin:
http://.../cgi-bin/guestbook.cgi?user=bob&rec=0
В этом случае шлюзу гостевой книги (guestbook.cgi) сообщается два параметра
user и rec, с помощью которых она может узнать "куда записывать" или "как обрабатывать" данные поступающие через поток ввода.
Считывание данных через поток sdtin должно осуществляться в динамическую память, или же во временный файл, если размер памяти ограничен или данные слишком велики для полного размещения в ОЗУ. С чем это связано ?
Это связано с тем, что при использовании статичестких буферов может произойти его переполнение.
Пример:
char cgi_data[1000];
...
long content_length=atol(getenv("CONTENT_LENGTH"));
fread(cgi_data,content_length,1,stdin);
...
Надеюсь все сразу видно :-)). Если content_length>1000, то произойдет переполнение cgi_data. Переполнение буферов это излюбленный метод атаки хакеров. Вместо этого лучше выделять память динамически:
char *cgi_data;
...
long content_length=atol(getenv("CONTENT_LENGTH"));
cgi_data=(char *)malloc(content_length);
if (cgi_data!=NULL)
fread(cgi_data,content_length,1,stdin);
...
После получения данных от сервера их надо еще декодировать. Можно это сделать сразу, а можно по мере надобности. Если Вы будете это делать сразу, то Вам также придется их разбить на куски, так как при декодировании могут появиться лишние знаки "&" и "=", которые больше не позволят вам отделять пары "имя=значение" друг от друга.
Вот пример процедуры, которая декодирует данные из буфера:
/* Возвращает верхний регистр символа*/
char upperchar(char ch)
{
if ((ch>=\'a\') && (ch<=\'z\'))
{
ch=\'A\'+(ch - \'a\');
return ch;
}
else return ch;
};
/* Переводит из Hex в Dec*/
char gethex(char ch)
{
ch=upperchar(ch);
if ((ch>=\'0\')&&(ch<= \'9\')) return (ch-\'0\');
if ((ch>=\'A\')&&( ch<=\'F\')) return (ch-\'A\'+10);
};
/*
Ищет и возвращает параметр с именем name, в buffer.
Если параметр name не найден, возвращает NULL.
Пример : message = getparam(post_buffer,"message=");
Замечание : символ "=" после имени параметра не удаляется
и входит в возвращаемый результат, поэтому рекомендуется
искать параметр вместе с символом "=".
*/
char *getparam(char *buffer,char *name)
{
if (buffer==NULL) return NULL;
char *pos;
long leng=512,i=0,j=0;
char h1,h2,Hex;
char *p=(char *)malloc(leng);
pos=strstr(buffer,name);
if (pos == NULL) return NULL;
if ((pos!=buffer) && (*(pos-1)!=\'&\')) return NULL;
pos+=strlen(name);
while ( (*(pos+i)!=\'&\')&&( *(pos+i)!=\'\\0\' ))
{
if ( *(pos+i)==\'%\' )
{
i++;
h1=gethex(*(pos+i));
i++;
h2=gethex(*(pos+i));
h1=h1<<4;
*(p+j)=h1+h2;
}
else
{
if (*(pos+i)!=\'+\') *(p+j)=*(pos+i);
else *(p+j)=\' \';
};
i++;
j++;
if (j >= leng) p=(char*)realloc(p,leng+20);
leng+=20;
};
if (j < leng) p=(char*)realloc(p,j+1);
*(p+j)=\'\\0\';
return p;
};
Теперь используя функцию getparam Вы сможете в любое время получить какой-нибудь параметр. Конечно эта процедура еще далека от совершенства, так как использует стандартный realloc и не обрабатывает ошибку нехватки памяти, но всеже ее можно использовать для данных не особо больших размеров.
Кстати надо еще сказать, что нецелесообразно размещать поступающие данные размером >64 Кб. в памяти, если, например, скрипт предназначен для закачки файлов, то можно (а вернее нужно) напрямую организовать запись в файл, иначе файл размером в пару мегабайт ваш скрипт не обработает :-)).
Шаг 4 - Обработка данных шлюзом.
Тут советов не много, а тем более наставлений, так как разработчик должен решать сам, что ему разместить внутри своей программы. Давайте поговорим о том как защитить, или лучше сказать, организовать эту работу.
Естественно, первое о чем Вы должны подумать, это как организовать возможность мнопользовательской работы скрипта. Т.е. скрипт должен отслеживать одновременный запуск нескольких копий программы. Почти все шлюзы в процессе работы производят запись или чтение каких-либо данных с диска. А одновременно записать в конец одного файла две запущенные программы не могут. Для этого и требуется отделить их друг от друга, чтобы не испортить результат их работы.
Думаю способов сделать это можно найти достаточно много, и в каждой операционной системе обязательно найдутся необходимые процедуры. Например, в Юниксах можно заблокировать на запись или чтение определенный участок файла.
Такие способы могут очень помочь, но они зачастую достаточно сложны и в повседневной жизни (вернее программировании :-) редко используются, поэтому у вас могут возникнуть определенные сложности на начальном этапе.
Есть способ, который просто реализовать и он не требует больших сил.
Он заключается в следующем. Ваша программа при запуске создает временный файл, например с именем lock.$. Такой же скрипт при запуске проверяет наличие этого файла, и если он не находит его, то создает его и продолжает работу, если же он его находит, то ждет несколько секунд и снова проверяет его наличие.
По окончании работы каждый скрипт обязан этот временный файл удалить. Т.е. получается программа будет ждать пока существует временный файл, созданный другой программой.
Пример реализации данного метода:
FILE f_lock;
/* ..... */
int counter;
/* Проверяем наличие временного файла */
while ((f_lock = fopen("lock.$", "r")) != NULL)
{
fclose(f_lock);
counter++;
if (counter>15000)
{
/*Сообщаем об ошибке и выходим */
printf("Error !!! Can\'t delete lock.$ file.");
return 1;
};
};
/* Создаем временный файл */
f_lock = fopen("lock.$", "w");
fclose(f_lock);
/*... Работа основной части скрипта ...*/
/* Удаляем временный файл */
while (remove("lock.$")!=0);
Как видно этот способ достаточно прост в реализации и точно убережет
Вас от неправильной работы скрипта.
Думаю к минусам этого метода надо отнести то, что он не позволяет выполнять свою работу другим запущенным скриптам, из-за того что и им приходится ждать.
Если у Вас происходят десятки обращений в минуту или даже секунду, то лучше конечно блокировать части файлов. Но это уже возможности самой системы, под которую расчитан скрипт (и не всегда это можно реализовать). А для обычных гостевых книг данный метод просто идеален.
Если уж мы как бы заговорили о быстродействии скриптов, то надо назвать один большой минус технологии CGI в целом. Заключается он в том, что при каждом обращении к скрипту веб-сервер запускает отдельную программу для обработки запроса, а это требует достаточно много ресурсов системы и процессорного времени.
В случае действительно больших нагрузок Вам придется подумать о реализации таких скриптов в совершенно ином виде, но, вероятно, встраивания кода шлюза в код Веб-сервера вы не сможете избежать.
Встроить код достаточно сложно, так как вы должны не только иметь код веб сервера (что иногда просто невозможно), но и знать как все это "чудо" работает.
На такое, естественно, способны не многие :-(. Хотя, написать свой сервер бывает очень полезно, для того чтобы на своей "шкуре" прочувствовать всю силу и сложность современных технологий :-))).
Шаг 5 - Вывод информации.
Для того, чтобы начать вывод данных шлюз должен сообщить браузеру тип выводимых данных и как с ними работать. Это действие производит заголовок.
Существуют два типа заголовков, вернее даже три, но один из них используется реже (о нем мы поговорим отдельно). Так вот, первый заголовок осуществляет переадресацию браузера на другой ресурс в сети и записывается вот как:
Location: URL-адрес ресурса
Под URL-адресом может быть что угодно, от заранее заготовленной страницы ответа, до страницы или файла на чужом сервере. Очень полезная штука для любителей преадресовки :-)
Пример:
Location: http://www.mjk.msk.ru/~dron/88.gif
Второй тип заголовка описывает данные которые будет выводить непосредственно скрипт. Задает их тип, длину и другие вспомогательные параметры. Самый простой заголовок записывается в виде:
Content-type: тип данных
Тип данных создается из двух составляющих: его типа и формата.
То есть, сначала указывается один из следующих типов:
- application - данные для какого-либо приложения
- audio - аудио данные, например RealAudio
- video - видео данные, например MPEG или AVI
- image - изображение
- text - текст
Далее через /(слэш) указывается формат данных.
Примеры заголовков:
Content-type: text/html
Content-type: image/gif
Content-type: video/mpeg
Еще заголовок может включать в себя вспомогательные параметры,
Content-length - длина содержимого, Expires - время существования в кэше и т.д.
Каждый параметр указывается в отдельной строке и заканчивается переносом строки \\n.
Пример:
Content-type: image/gif
Content-length: 1052
После того как заголовок сформирован и отправлен надо отправить пустую строку, для того, чтобы отделить его от основных данных.
...
printf("Content-type: text/html\\n");
printf("\\n");
printf("<html><h1>Simple CGI for example !!!</h1></html>");
...
Если отделяющая пустая строка (я ее сделал отдельной строкой в коде) не будет отправлена, то сервер выдаст ошибку 500 Internal Server Error : Bad header.
После отправки заголовка скрипт может приступать к выводу данных, но естественно только в указанном им формате.
Шаг 6 - Переменные среды о сервере.
Как я уже говорил, сервер и шлюз общаются между собой через стандартные потоки ввода/вывода и переменные среды. Незная названий этих переменных среды сложно что-либо получить от сервера :-).
Давайте рассмотрим какие же переменные среды устанавливает сервер в момент запуска шлюза.
Переменные среды о сервере
Эти переменные сервер устанавливает для того, чтобы шлюз мог узнать с каким сервером он работает. Сюда входят данные о портах сервера, его версии, типе интерфеса CGI и т.д. В каждой версии сервера часто прибавляются новые переменные, но следующие переменные должен устанавливать любой сервер любой версии.
- GATEWAY_INTERFACE
- Указывает версию интерфейса CGI, который поддерживает сервер. Например:
CGI/1.1
- SERVER_NAME
- Содержит IP адрес сервера или его доменное имя. Например:
www.mjk.msk.ru
- SERVER_PORT
- Номер порта, по которому сервер получает http запросы. Стандартный порт для этого 80.
- SERVER_PROTOCOL
- Версия протокола Http, который использует сервер для обработки запросов. Например:
HTTP/1.1
- SERVER_SOFTWARE
- Название и версия программы сервера. Например:
Apache/1.3.3 (Unix) (Red Hat/Linux)
Эти переменные обеспечивают все необходимые данные о сервере, на котором запускается скрипт.
Если Ваш сервер сконфигурирован для работы с одним хостом, то скорее информация эта вам не понадобится.
Сейчас же большинство серверов позволяют создавать так называемые "виртуальные" хосты. Т.е. это один компьютер, который поддерживает много IP адресов и различает запросы от клиентов по требуемому хосту, на которые он соответственно выдает странички с сайтов.
Тут уже могут понадобиться данные о портах сервера (т.к. многие хосты просто "сидят" на других портах, например 8080, 8081 и т.д.) и его IP адрес с именем.
Шаг 7 - Переменные среды о запросе.
При помощи переменных данного типа шлюз узнает полную информацию о запросе к нему.
Т.е. каким методом будут передаваться данные, их тип, длину и т.д.
- AUTH_TYPE
- Тип авторизации используемой сервером. Например:
Basic
Подробнее об авторизации в сервере Apache читайте в "Шаг 18 - Авторизация посетителей"(Html&Web).
- CONTENT_FILE
- Путь к файлу с полученными данными. Используется только в серверах под Windows. Например:
c:\\website\\cgi-temp\\103421.dat
- CONTENT_LENGTH
- Длинна переданной информации в байтах. То бишь сколько надо считать байтов из stdin. Например:
10353
- CONTENT_TYPE
- Тип содержимого посланного серверу клиентом. Например:
text/html
- OUTPUT_FILE
- Файл для вывода данных, используется только серверами под Windows. Аналогично CONTENT_FILE.
- PATH_INFO и PATH_TRANSLATED
- В современных веб-серверах появилась возможность после имени скрипта указывать еще какой-то определенный путь. Для чего он нужен скрипту я пока не очень понимаю.
Но видимо некоторым он сможет пригодиться. Эти переменные работают следующим образом.
Предположим существует скрипт с именем 1.cgi в каталоге сервера /cgi-bin, тогда при вызове скрипта в таком виде:
http://.../cgi-bin/1.cgi/dir1/dir2
данные переменные установятся следующим образом:
PATH_INFO=/dir1/dir2
PATH_TRANSLATED=/home/httpd/html/dir1/dir2
Помоему видно, что эти переменные будут указывать на папку относительно корневой директории сервера. При этом PATH_TRANSLATED будет содержать абсолютный путь до этого каталога на диске сервера. В данном случае корневым каталогом сервера считается
/home/httpd/html/, и еще замечу, что это путь в Unix системах.
Под dos/win системами переменная PATH_INFO не изменится, а
PATH_TRANSLATED будет содержать d:\\apache\\htdocs\\dir1\\dir2 (в данном случае корнем сервера является директория d:\\apache\\htdocs\\).
- QUERY_STRING
- Содержит данные переданные через URL. Такие данные указываются после имени шлюза и знака ?. Пример:
http://.../cgi-bin/1.cgi?d=123&name=kostia
тогда переменная QUERY_STRING будет содержать
d=123&name=kostia
и еще незабывайте, что данные передаваемые таким образом кодируются методом URL.
- REMOTE_ADDR
- Содержит IP адрес пользователя пославшего запрос шлюзу. Если Вы обращаетесь
к любому шлюзу в интернете, то данная переменная будет содержать ваш IP адрес. Пример:
192.168.1.36
- REMOTE_HOST
- Содержит ваше доменное имя, при условии, что вы прописаны на каком-либо DNS сервере.
Например, если ваш Dial-UP провайдер регистрирует все свои динамические IP адреса на DNS сервере, то
при обращении к шлюзу, эта переменная может содержать примерно следующее:
d6032.dialup.cornell.edu
или
dial57127.mtu-net.ru
или
ppp-130-66.dialup.metrocom.ru
(брал прямо из логов сервера :-)
- REQUEST_METHOD
- Мы раньше говорили об этой переменной. Она содержит метод передачи данных шлюзу: GET или POST.
- REQUEST_LINE
- Содержит строку из запроса протокола HTTP. Например:
GET /cgi-bin/1.cgi HTTP/1.0
- SCRIPT_NAME
- Содержит имя вызванного скрипта. Например: 1.cgi.
Все эти переменные, надеюсь, обеспечат Вам все самые необходимые данные о запросе к шлюзу.
Шаг 8 - Переменные среды о клиенте.
Осталось рассмотреть еще несколько переменных, которые несут в себе информацию о клиенте пославшем запрос.
Их не много, всего три:
- HTTP_ACCEPT
- Эта переменная пречисляет все типы данных, которые может получать и обрабатывать клиент.
Часто содержит просто */*, т.е. клиент может получать все подряд. Пример:
*/*,image/gif,image/x-xbitmap
- HTTP_REFERER
- Содержит URL страницы, с которой был произведен запрос, т.е. которая
содержит ссылку на шлюз. Пример:
http://www.mjk.msk.ru/~dron/index.html
- HTTP_USER_AGENT
- Содержит в себе название и версию браузера, которым пользуется клиент для запроса.
Полезно для того, чтобы игнорировать браузеры ненавистных вам фирм :-))). Пример:
MyBrowser/1.0 (MSIE 4.0 Compatible, Win98)
В большинстве случаев эти переменные не несут в себе полезной для скрипта информации, если только Вы не будете передавать все данные в формате MS Word , и вам не потребуется знать сможет ли клиент их обработать :-).
С моей точки зрения полезной можной назвать только HTTP_REFERER, которая может использоваться для защиты шлюза, например, запуска только с определенной страницы. Большинство счетчиков пользуется этой переменной для того, чтобы не учитывать хиты с других хостов, на которые этот счетчик не зарегистрирован.
Еще существует несколько переменных, которые тоже относятся к переменным о клиенте, но почему-то не все браузеры их сообщают и поэтому пользоваться ими надо, как говорится: "на свой страх и риск" :-))). Вот они:
- HTTP_ACCEPT_ENCODING
- Указывает набор кодировок, которые может получать клиент. Например:
koi8-r, gzip, deflate
- HTTP_ACCEPT_LANGUAGE
- Содержит в себе список языков в кодах ISO, которые может принимать клиент. Например:
ru, en, fr
- HTTP_IF_MODIFIED_SINCE
- Содержит в себе дату, новее которой должны быть получаемые данные.
- HTTP_FROM
- Содержит список почтовых адресов клиента.
Вобщем-то весь этот список никем не ограничивается и любой браузер может сообщить кроме этих переменных еще добрый десяток. Правда уважающие себя разработчики не будут этого делать. А зачем ?
Шаг 9 - Обработка простой формы.
Понятие формы языка html описано в шаге 19 раздела Html&Web.
Давайте сделаем простую форму.
<form action="http://localhost/cgi-bin/primer.cgi" method=GET>
Введите свое имя пользователя:
<input type=text maxlength=150 name=user>
<p><input type=submit value=Send>
</form>
Мы уже обсуждали методы передачи данных в шлюз раньше. Какие тут могут быть советы ?
Лично мне кажется передавать такие маленькие формы лучше всего посредством метода GET.
С чем это связано ? Во-первых получить данные из переменной окружения намного легче, чем считать их из потока.
Чтобы считать данные из потока надо точно знать их размер, позаботиться о выделении памяти и многом другом. Тут же обо всем позаботится сервер и встроенные средства программирования вашего языка. Во-вторых пользователь сможет обратиться к вашему скрипту непосредственно из адресной строки браузера.
Например, многие программы для поиска информации в интернете используют различные поисковые сервера. Для того чтобы сделать запрос к одному из них требуется всего лишь вызвать браузер и сообщить ему URL. В Windows это делается просто в командной строке (а значит и просто сделать программе):
start http://www.abc.com/cgi-bin/search.cgi?word=hello&language=ru
Таким образом программа может сразу вызвать браузер с уже подготовленной страничкой и пользователю не прийдется даже знать адрес этого поисковика и как он работает, все знает программа.
С методом POST в этом отношении сложнее, для этого программе нужно уметь работать по протоколу HTTP и связываться с серверами в интернете. Размер и сложность такой программы будет на порядок выше.
Поэтому, если Ваш ресурс может быть полезен и при этом передаваемые ему данные не будут превышать 32 Кб (это ограничение на длинну URL, если я все правильно помню :-), то лучше метода GET не найти.
Давайте напишем программу для обработки этой формы.
#include <stdio.h>
#include <stdlib.h>
//Здесь надо вставить процедуру получения
//параметра по его имени... Она была описана
//раньше.
char *getparam(...)
{
};
int main()
{
char *user=NULL;
char *content=NULL;
char *request_method=getenv("REQUEST_METHOD");
if (strcmp(request_method,"GET")!=0)
{
printf("Content-type: text/html\\n\\n");
printf("Unknown REQUEST_METHOD. Use only GET !\\n");
return -1;
};
content=getenv("QUERY_STRING");
user=getparam(content,"user=");
printf("Content-type: text/html\\n\\n");
printf("User name=\\"%s\\"\\n",user);
};
После того как вся программа будет собрана и откомпилированна, расположите ее в директории cgi-bin вашего вебсервера. Теперь можно смело пробовать форму написанную выше. Я для теста использовал браузер напрямую... Как я уже говорил, достаточно набрать
http://localhost/cgi-bin/primer.cgi?user=hello
И Вы увидите тот же результат, что и при использовании формы. Кстати вот он:
User name="hello"
Обратите внимание на то, что я уже в первой программе начинаю предусматривать возможные ошибки, и поэтому неизвестные программе методы она не будет выполнять. Вообще Вы должны приучиться к этому сразу, т.е. отлавливать возможные ошибки уже на этапе создания шлюза, иначе без этого он может стать большой дырой в системе безопастности вашего сервера.
Я по ходу шагов еще тысячу раз упомяну Вам об этом, но только потому, что это действительно очень важно.
Шаг 10 - Как запускать CGI.
Если у Вас от чего-то все работает, то очень СТРАННО. Как правило новичку, чтобы запустить скрипт приходится попотеть. Какие же основные проблемы возникают при запуске скрипта ?
Настройки сервера
Первой, но не главной причиной может послужить неправильная настройка сервера.
Скорее всего попросту он не имеет права запускать скрипты из этой директории.
Я всегда рассматриваю только Web-сервер Apache, поэтому приведу настройки для него. Кстати сегодня в ComputerWorld(8.02.2000) опубликовали результаты исследований RUNet\'a. Оказалось, что Apache установлен на 78% всех серверов, веб-сервера от Microsoft - 19%, доля остальных серверов составляет по 1%.
Так что я опять говорю, лучше Apache Вы не найдете !!!
Ладно отвлеклись от темы... Все настройки Apache для каждой директории задаются
с помощью файла .htaccess. Если такового в вашей директории не имеется, то
создавайте его. В него запишите следующее:
Options ExecCGI
Или даже посоветую обратиться к документации, но думаю она не понадобится.
Теперь любые скрипты в этом каталоге будут загружаться без проблем.
Неверные атрибуты на файле скрипта
Если на вашем сервере установлена система подобная Windows, то эта проблема
вас не касается, так как все программы *.exe "эта" система загружает без вопросов.
В случае если Ваша система Unix, то вам повезло меньше, особенно если
Вы до этого видели только Windows.
В кратце поясню... Во всех системах Unix для каждого файла устанавливаются атрибуты файлов. Этих атрибутов (как правило) девять. Даю список таковых.
Owner Read
Owner Write
Owner Execute
Group Read
Group Write
Group Execute
Other Read
Other Write
Other Execute
Если кратко, то в Unix системах создаются пользователи и разделяются на группы. Вы вот при входе в систему набираете свой логин и пароль, т.е. Вы являетесь пользователем. Также по FTP и т.д. Все кто имеет доступ к системе является ее пользователем.
Атрибуты типа Owner задают параметры для Вас, т.е. для владельца файлов.
Атрибуты Group определяют уровень доступа для вашей группы, т.е. если Вы принадлежите к группе
Webmasters, то при установке атрибута Group Write любой другой пользователь, который принадлежит к группе Webmasters сможет записывать в этот файл информацию.
Думаю для чего Other понятно, это значит всем остальным.
При записывании файла через FTP атрибуты файла устанавливаются по умолчанию
rw.r..r..
Т.е. Вы можете писать и читать, а остальные могут только читать. Как видите ни один атрибут не указывает на то, что файл загружаемый.
Вы должны добиться такого:
rwxr.xr.x
Т.е. установить атрибут Execute во всех группах.
Как это сделать это другой вопрос, давайте рассмотрим с вами работу с некоторыми FTP клиентами.
Сразу скажу, что не использую никакие Виндовые программы, т.е. графические "проги" рассчитанные на любителей делать все одним кликом мыши...
Я пользуюсь нашим, т.е. российским файловым менеджером FAR, если у вас его нет, то Вы много потеряли... И я вам сочувствую.
Так вот в нем надо нажать Ctrl-A на том файле, который Вы закачали на сервер (только делаете это не на локальном диске, а на FTP, а то увидите вместо атрибутов Unix атрибуты Досовской файловой системы)
Делаете следующую картинку:
R W X R W X R W X
[x][x][x] [x][ ][x] [x][ ][x]
И нажимаете Okey. Теперь все классно.
Если у Вас нет FAR, то у Вас ОБЯЗАНА быть программа в системе, которая занимается сервисом FTP. В большинстве систем (и в Винде) такая программа называется ftp.
Запустите ее. Наверняка она обладает только командной строкой, так что потейте... :-) Я рассмотрю программу ftp.exe, которая входит в виндовс.
Первое, что надо сделать открыть Ваш сайт, делается это командой open
ftp> open www.mjk.msk.ru
Связь с mjk.
220 mjk-gw.mjk.msk.ru FTP server (Version wu-2.4.2-academ[BETA-18](1)
Mon Aug 3 19:17:20 EDT 1998) ready.
Пользователь (mjk:(none)): dron
331 Password required for dron.
Пароль:
230-Please read the file README.linux
230- it was last modified on Sat Feb 5 16:31:50 2000 - 3 days ago
230 User dron logged in.
ftp>
Теперь Вы в системе. Наберите help для получения основных команд. Попробуйте набрать dir. Пример вывода:
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
total 48
drwx------ 3 dron mjkusers 1024 Feb 6 16:58 .
drwxr-xr-x 75 root root 2048 Feb 1 00:03 ..
-rw-r--r-- 1 dron mjkusers 1155 Jun 24 1999 .Xdefaults
-rw------- 1 dron mjkusers 24 Jan 8 11:35 .bash_history
-rw-r--r-- 1 dron mjkusers 24 Jun 24 1999 .bash_logout
-rw-r--r-- 1 dron mjkusers 230 Jun 24 1999 .bash_profile
-rw-r--r-- 1 dron mjkusers 124 Jun 24 1999 .bashrc
-rw-r--r-- 3 dron mjkusers 1324 Jan 8 11:32 123.cgi
-rw-r--r-- 1 dron mjkusers 37165 Feb 5 16:31 README.linux
226 Transfer complete.
691 байт получено за 0.33 с (2.09 КБ/с)
Мне например надо теперь установить атрибут загрузки на файл 123.cgi, как видите у него такого атрибута нет.
Такую возможность ftp.exe не предоставляет, зато он может посылать команды непосредственно FTP - серверу, т.е. что нам и требуется.
Если вы вызывали помощь, то знаете, что такую функцию выполняет команда quote. Чтобы Вам особо не разбираться просто приведу команду.
quote SITE CHMOD 755 123.cgi
Теперь на файле 123.cgi будут установлены необходимые атрибуты. Если Ваш файл располагается в другой директории, то пользуйтесь командой cd (change directory).
Неправильный атрибут на каталоге скрипта
Многие скрипты не только выводят какую-то информацию, но и еще записывают что-то в определенные папки или файлы. Тут надо предусмотреть правильный доступ к этим ресурсам.
Любой Web-сервер работает не от вашего имени, а от другого, поэтому запуская скрипт он не предоставляет ему Ваши возможности. Вы должны поставить атрибуты на директорию, в которую записан скрипт, для полного доступа, т.е. для записи всем кому угодно.
Для установки таких атрибутов в FAR\'е поставьте все крестики. В ftp.exe команда такая
quote SITE CHMOD 777 <директория>
Хочу Вас также предостеречь, делая полный доступ на директорию помните, что любой "злоумышленник" может вам подпортить жизнь, стереть Ваш скрипт и например записать свой, или записывать неправильные данные в ваши файлы. Короче он может делать, что угодно. Особенно опасно делать полный доступ к директории в которой лежит страничка, потому как рано или поздно вы ее там не обнаружите :-(.
Поэтому СОВЕТ. Создавайте специально для скриптов отдельные каталоги и используйте их для записи данных. А лучше всего делать доступ только отдельному файлу (в этом случае маска доступа не 777, а 666 !!!)
Причем думаю не плохо было бы позаботиться и о шифровании, т.к. любой скрипт может записывать конфиденциальную информацию, такую как номера кридитных карт, почтовые адреса, имена и фамилии. Любой "спаммер" или "хакер" скажет вам большое спасибо за такой подарок, хотя скорее всего "спасибо" вам скажут "дяди в погонах".
Вобщем-то все, надеюсь у Вас все заработает :-)
Шаг 11 - Как работают гостевые книги ?
Гостевая книга очень полезная штука на сайте, да и что тут объяснять ? Все хотят иметь на сайте хоть какой-то механизм обратной связи. Именно для этого и существуют гостевые книги.
Общий метод, я думаю, прост и ясен всем. Вы создаете страничку с формой, которую будут заполнять ваши посетители и посылать вам. С этим думаю не будет возникать проблем. Проблемы возникнут, когда надо будет писать обрабатывающий скрипт. Я могу лишь дать некоторые советы для создания хороших гостевых книг.
Первое о чем надо позаботиться, это обеспечение обработки нескольких пользователей одновременно. Думаю не секрет, что в интернете миллионы людей, и никогда не известно кто из них зайдет на вашу страничку, а самое главное когда... Вероятность появления ситуации, что два или более пользователя одновременно записывают данные в гостевую книгу очевидна. Да это и вовсе не относится только к гостевым книгам, существуют другие более "нагруженные" приложения, такие как чаты, форумы, счетчики и т.д.
Второе, что я считаю важным - это обеспечение настроек формата вывода данных. Очень важно для того, чтобы использовать один и тот же скрипт где угодно и когда угодно. Если Ваш скрипт "не мобилен", то Вам прийдется для добавления какой-нибудь новой информации переписывать скрипт.
Ну, а третье может всем и не нужно, хотя стоило бы обеспечить обработку сразу нескольких гостевых книг одним скриптом. Это позволит пользоваться гостевыми книгами не только Вам.
Если соблюдать такие вот простые правила можно создать не плохие скрипты, обеспеченные стойкостью и мобильностью.
О том как контролировать одновременные вызовы скрипта несколькими клиентами я писал раньше... Если Вы не помните, то "алгоритм" прост. При запуске скрипт создает какой-либо файл на время работы, затем его удаляет. Другой же запущенный скрипт контролирует появление этого файла, и если он есть, то ждет некоторое время пока он не исчезнет.
Как обеспечить настройки выводимой информации я могу лишь только посоветовать, потому как каждый может придумать разные способы и методы. Лично моя гостевая книга устроена в этом отношении просто.
Есть файл, который полностью описывает структуру нового добавляемого сообщения. Т.е. это своего рода файл html, который скрипт попросту копирует в гостевую книгу, НО при этом подставляет какие-то данные в обозначенные места.
Как их обозначить ? Это тоже Вам решать... Я обозначал просто, между двумя амперсандами & задается имя поля формы, значение которого надо записать в гостевую книгу. Таким образом изначально сам скрипт гостевой книги даже не знает какие поля формы ему сообщаются.
Давайте приведу кусок кода, который реализует чтение файла с форматом и подставляет вместо полей их содержимое.
char parname[150];
char *par;
int parnamelen=0;
char charbuf;
int ampfound=0, dolfound=0;
if ( (f_form=fopen(guest_path,"rt")) != NULL )
{
if ( (f_guest = fopen(guestbook,"at")) != NULL)
{
while (!feof(f_form))
{
fscanf(f_form,"%c",&charbuf);
if ((ampfound==0)&&(dolfound==0))
{
if ((charbuf!=\'&\') && (charbuf!=\'$\'))
{
fprintf(f_guest,"%c",charbuf);
}
else
{
if (charbuf==\'&\') ampfound=1; else dolfound=1;
parnamelen=0;
};
}
else
{
if ((charbuf==\'&\') || (charbuf==\'$\'))
{
if (parnamelen==0)
{
if (charbuf==\'&\') fprintf(f_guest,"&");
else fprintf(f_guest,"$");
}
else
{
parname[parnamelen]=\'\\0\';
if (ampfound==1) par=getparam(buf,parname);
else par=getenv(parname);
fprintf(f_guest,"%s",par);
};
if (ampfound==1) ampfound=0; else dolfound=0;
}
else
{
parname[parnamelen]=charbuf;
parnamelen++;
};
};
};
fclose(f_guest);
};
fclose(f_form);
};
Алгоритм прост, считываете посимвольно, если встречается амперсанд, то начинаете накапливать имя поля, которое необходимо вставить в гостевую книгу. Ну, а когда дойдете до второго амперсанда, то выводите содержимое этого параметра в файл гостевой книги.
Забыл сказать, да Вы наверно это уже заметили, что я еще использую знак доллара, он служит для того чтобы выводить не содержимое формы, а переменные среды. Это очень полезно, если Вы хотите записывать в гостевую книгу какие-то данные о соединении, например, IP адрес клиента.
Ну, а третье условие "хорошей" гостевой книги - один скрипт на всех :), попробуйте реализовать сами, это не очень сложно... Создайте конфигурационный файл для гостевой книги, в котором Вы будете прописывать положение файлов других гостевых книг.
Тут также надо будет позаботиться о выборе гостевой книги, т.е. в какую гостевую книгу будут записываться поступаемые данные. Я для этого рекомендую использовать метод GET, а сами данные будете посылать через POST.
Конечно тут я описал самые "зачатки" продвинутой гостевой книги, но у нас еще все впереди. Можно сделать автоматическое занесение в архив устаревших записей, затем если сообщений слишком много, то создавать перелистывающиеся страницы и еще много чего...
Шаг 12 - Посылка письма.
Посылка писем внутри скрипта бывает очень полезной. Имея такую возможность Вы, например, можете оповещать себя или еще кого-то о запуске или работе скрипта. Например, очень хорошо использовать посылку писем в регистрационных скриптах для подтверждения по почте пользователя о том, что он зарегистрирован.
Думаю можно найти сотни применений для этой технологии, причем иногда настолько простых и эффективных, что трудно будет найти нечто другое.
Ну, а теперь от слов к делу. Сразу вопрос: Вы знаете как работать по протоколу SMTP ? Я вообще-то знаю, но реализовывать этот механизм в каждом скрипте что-то не хочется. Слишком это сложно и громоздко.
А потом не уверен я, что любой может без проблем написать программу работающую с сокетами, да еще и по определенному прококолу. Вот как раз для таких функций, т.е. отправки почты, существуют
программы работающие через командную строку. На большинстве серверов, да и вообще компьютерах с Unix системами, функцию отправки писем может
возлагать на себя непосредственно SMTP демон. Их существует достаточно много, хотя если я скажу, что самый распространенный из них это sendmail, то не сильно ошибусь.
Конкретно sendmail позволяет посылать письмо из командной строки. Как это делать лучше посмотреть (да и вообще лучше всегда смотреть :) в мануале к программе. В моем случае :) чтобы послать письмо надо набрать следующую командную стоку:
/usr/lib/sendmail -t
Далее набирается письмо в формате:
To: email@adr
From: mymail@adr
Subject: Hello !!!
This Is Letter !!!
.
Признаком конца письма служит строка с одной единственной точкой, поэтому прошу Вас обратить на это внимание.
Все хорошо, но как все это дело использовать в скрипте ? А просто !!! В Юниксе существует уникальная просто возможность запускать программу и ассоциировать с ней файловые потоки для обмена информацией !!!
Делает такой вызов программы функция практически аналогичная fopen, но только popen :-) !!! Так что теперь делаем просто файловый указатель и открываем нашу программу посылки письма.
#include ...
FILE *sendmail;
sendmail=popen("/usr/lib/sendmail -t","w");
fprintf(sendmail,"To: email@adr\\n");
fprintf(sendmail,"From: myemail@adr\\n");
fprintf(sendmail,"Subject: Hello\\n");
fprintf(sendmail," HEHEHEHEH !!!\\n");
fprintf(sendmail,".\\n");
pclose(sendmail);
Теперь ваше письмо пошлется другу :). Можно ли использовать такой механизм в Виндовсе ? Не знаю :). По крайней мере стандартная программа посылки почты, т.е. OutLook не позволит вам этого сделать совершенно точно, да и кроме того ДОС-система не умеет вызывать программу как файл !!! У Юникса здесь нет конкурентов :).
Остается только найти программу под Виндовс, которая будет посылать письма из командной строки, а у меня такая раньше была. И вызывать ее через стандартую exec-процедуру. Только при этом не забывайте создавать временный файл с почтовым сообщением !!! :)
Хотя если Вы будете писать программы только для Unix, то у Вас не возникнет проблем :)
Шаг 13 - Разработка класса CGIContent.
Код получения параметров во многих скриптах CGI одинаков и кочует от одного скрипта к другому практически без изменений.
Поэтому стоит написать библиотеку классов для работы с CGI и подключать ее при разработке нового скрипта.
Прежде чем разработать какую-либо программу надо поставить себе цель, поэтому давайте ее наметим. Естественно самое главное, что должны обеспечивать классы - это простоту работы с ними.
Потом любой скрипт должен уметь получать данные большого размера, при этом обеспечивая их удобное размещение в памяти и маленький размер.
До сих пор мы просто выделяли кусок памяти и целиком помещали в него все переданные данные. Из-за этого при передаче огромного количества данных скрипт мог зависнуть. Кроме того наши данные никогда не декодировались и хранились закодированными, что сильно тратило нашу память и содержимое параметров могло занимать в три раза больше, чем на самом деле.
Первое, что нам потребуется для разработки нашего класса - это класс для хранения больших объемов данных. Самым простым способом хранения помоему является список. Поэтому создадим класс CGIContentItem, который будет элементом списка управляемого классом CGIContent:
class CGIContent;
class CGIContentItem{
friend CGIContent;
char *Content;
CGIContentItem *Next;
public:
CGIContentItem();
~CGIContentItem();
};
Простой элемент списка у нас будет содержать текстовую строку Content и указывать на следующий элемент. Реализация деструктора и конструктора тоже просты:
CGIContentItem::CGIContentItem(){
Content=NULL;
Next=NULL;
};
CGIContentItem::~CGIContentItem(){
if (Content!=NULL) free(Content);
if (Next!=NULL) delete Next;
};
Для всех классов элементы нашей ячейки скрыты, поэтому класс CGIContent, который является управляющей оболочкой для списка описан как дружественный.
Теперь требуется реализовать сам этот класс, чтобы получить полнофункциональный список со всякими возможностями.
class CGIContent{
public:
CGIContentItem *Content;
CGIContent();
~CGIContent();
CGIContentItem* Add(char *Buffer);
int Write(FILE *f_out);
};
Самое главное в списке это указатель на его начало, эту функцию у нас выполняет указатель Content.
Для начала нам потребуется только функция добавления в список Add() и вывода Write(). Для возможности вывода содержимого списка в любое место в функции Write() надо использовать указатель типа FILE, вместо которого можно будет поставлять поток stdout или любой другой открытый файл. Вот реализация методов этого класса:
CGIContent::CGIContent(){
Content=NULL;
};
CGIContent::~CGIContent(){
if (Content!=NULL) delete Content;
};
CGIContentItem* CGIContent::Add(char *Buffer){
CGIContentItem *temp,*p;
temp=new CGIContentItem();
temp->Content=strdup(Buffer);
if (Content==NULL){
Content=temp;
} else {
p=Content;
while (p->Next!=NULL) p=p->Next;
p->Next=temp;
};
return temp;
};
int CGIContent::Write(FILE *f_out){
CGIContentItem *temp=Content;
while (temp!=NULL){
fprintf(f_out,"%s",temp->Content);
temp=temp->Next;
};
return 0;
};
Теперь у нас готов полнофункциональный класс способный хранить в себе большие массивы информации.
Шаг 14 - Класс CGIParam.
Вторым классом, который требуется для класса CGIApp должен быть класс хранящий имена передаваемых параметров и их значение.
Для хранения значений параметров мы уже сделали класс CGIContent. Нам осталось добавить в класс переменную для имени и методы для обеспечения добавления параметров и их быстрого поиска.
Структура класса очень похожа на класс CGIContent. Класс CGIParamItem будет элементом списка, а класс CGIParam управляющим:
class CGIParam;
class CGIParamItem {
friend CGIParam;
char *Name;
CGIContent *Content;
CGIParamItem *Next;
public:
CGIParamItem();
~CGIParamItem();
};
CGIParamItem::CGIParamItem(){
Name=NULL;
Next=NULL;
Content=new CGIContent();
};
CGIParamItem::~CGIParamItem(){
if (Name!=NULL) free(Name);
delete Content;
if (Next!=NULL) delete Next;
};
Класс CGIParamItem содержит имя параметра в переменной Name, а содержимое в Content.
Теперь класс CGIParam:
class CGIParam{
CGIParamItem *List;
public:
CGIParam();
~CGIParam();
CGIContent *Add(char *name,char *Buffer);
CGIContent *Find(char *name);
};
CGIParam::CGIParam(){
List=NULL;
};
CGIParam::~CGIParam(){
if (List!=NULL) delete List;
};
CGIContent *CGIParam::Add(char *name,char *Buffer){
CGIParamItem *temp,*p;
temp=new CGIParamItem();
temp->Name=strdup(name);
temp->Content->Add(Buffer);
if (List==NULL){
List=temp;
} else {
p=List;
while (p->Next!=NULL) p=p->Next;
p->Next=temp;
};
return temp->Content;
};
CGIContent *CGIParam::Find(char *name){
CGIParamItem *temp=List;
while (temp!=NULL){
if (strcmp(temp->Name,name)==0) return temp->Content;
temp=temp->Next;
};
return NULL;
};
Удобство этого класса очевидно. Позволяет хранить большие объемы, обеспечивает очень быстрый поиск нужного параметра. Теперь вместо просмотра всех данных, как раньше, требуется всего лишь просмотреть имена параметров, что, собственно говоря, и требуется.
Кроме того эти классы в будущем будут хранить уже декодированные данные. Раньше нам нельзя было декодировать данные потому, что мы могли потерять связки разделяющие имена параметров и их содержимое между собой, т.е. могли бы появиться нежелательные символы & и = и испортить все картину.
Теперь этого никогда не произойдет, т.к. данные хранятся отдельно друг от друга и бережно охраняются своими управляющими классами. Преимущество ООП вообще еще состоит в и том, что создав класс вы можете не беспокойться о динамическом распределении памяти, правильно написанные конструкторы и деструкторы будут заботиться об этом без вашего участия.
Шаг 15 - Класс CGIApp.
Давайте теперь подведем черту под тем, что было сделано в прошлых шагах. Все эти классы были предназначены для одного самого главного CGIApp.
Вот его описание:
class CGIApp{
public:
CGIParam *Param;
int Method;
char *Query_String;
long Content_Length;
CGIApp();
~CGIApp();
int Init();
CGIContent *FindParam(char *name);
};
Все просто как всегда, Param хранит полученные данные, Method содержит данные о методе передачи данных, который определяется следующими константами:
#define METHOD_GET 1
#define METHOD_POST 2
Переменные Query_String и Content_Length содержат значения полученные из соответствующих переменных окружения.
В конструкторе и деструкторе класса также все по старому:
CGIApp::CGIApp(){
Param=new CGIParam();
Query_String=NULL;
Method=0;
Content_Length=0;
};
CGIApp::~CGIApp(){
delete Param;
if (Query_String!=NULL) free(Query_String);
};
Особую "ценность" содержит в себе метод Init(). Эта процедура инициализирует все данные необходимые для работы приложения CGI, т.е. получает и декодирует все полученные от клиента данные. Сначала приведу текст этого метода:
int CGIApp::Init(){
char *Buffer;
char *Content_Len=NULL;
char *Query_Str=NULL;
char *ParamName=NULL;
long Buffer_Len=0;
long Buffer_Size=0;
char Mode=0, ToMode=0;
long i=0;
char ch=0;
CGIContent *temp=NULL;
Buffer=getenv("REQUEST_METHOD");//получаем метод передачи данных
if (Buffer==NULL) return 0;//если нет такой переменной то ошибка
Query_Str=getenv("QUERY_STRING");//строка в URL после "?"
Content_Len=getenv("CONTENT_LENGTH");//длинна передаваемых данных
if (strcmp(Buffer,"GET")==0){
if (Query_Str!=NULL) Content_Length=strlen(Query_Str);
else Content_Length=0;
Method=METHOD_GET;
} else {
if (strcmp(Buffer,"POST")==0){
Method=METHOD_POST;
if (Content_Len!=NULL) Content_Length=atol(Content_Len);
else Content_Len=0;
} else goto proc_exit;
};
Buffer=NULL;
Buffer_Size=10000;
//инициализируем буффер для работы
while (Buffer==NULL){
Buffer=(char *)malloc(Buffer_Size);
if (Buffer==NULL){
if ((Buffer_Size-=1000)<0) return 0;
};
};
//основной цикл
while (i<Content_Length){
//получаем следующий байт данных в
//зависимости от метода передачи
switch (Method){
case METHOD_GET:{
ch=*(Query_Str+i);
break;
};
case METHOD_POST:{
fread(&ch,1,1,stdin);
break;
};
};//switch;
switch (Mode){
case 0:{//режим накопления имени параметра
if (ch!=\'=\'){
if (ch==\'+\') ch=\' \'; else
if (ch==\'%\'){
ToMode=0;
Mode=2;
break;
};
*(Buffer+Buffer_Len)=ch;
Buffer_Len++;
} else {
Mode=1;
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL) free(ParamName);
ParamName=strdup(Buffer);
Buffer_Len=0;
};
break;
};//case 0;
case 1:{//режим накопления содержимого атрибута
if (ch!=\'&\'){
if (ch==\'+\') ch=\' \'; else
if (ch==\'%\'){
ToMode=1;
Mode=2;
break;
};
*(Buffer+Buffer_Len)=ch;
if ((Buffer_Len+=1)>=Buffer_Size){
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL){
temp=Param->Add(ParamName,Buffer);
free(ParamName);
ParamName=NULL;
} else {
temp->Add(Buffer);
};
Buffer_Len=0;
};
} else {//если найден символ &, то добавляется параметр и значение
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL)
temp=Param->Add(ParamName,Buffer);
else
temp->Add(Buffer);
Buffer_Len=0;
Mode=0;
};
break;
};//case 1;
case 2:{//режим преобразования из HEX первого символа после %
*(Buffer+Buffer_Len)=(gethex(ch)<<4);
Mode=3;
break;
};//case
case 3:{//второго символа HEX
*(Buffer+Buffer_Len)+=gethex(ch);
Buffer_Len++;
Mode=ToMode;
break;
};
};//switch (Mode)
i++;
};
//если осталось что-то после работы то добавляем
if (Buffer_Len>0){
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL)
temp=Param->Add(ParamName,Buffer);
else
temp->Add(Buffer);
};
proc_exit:
if ((Method==METHOD_GET)&&(Query_Str!=NULL))
Query_String=strdup(Query_Str);
return 0;
};
Этот метод получает все переменные окружения необходимые для считывания приходящих данных. После чего инициализирует буффер и считывает их в зависимости от метода передачи.
В зависимости от считанного символа происходит смена режима и символ добавляется либо в имя параметра, либо в содержимое, либо декодируется.
Последний на этот момент необходимый метод поиска:
CGIContent *CGIApp::FindParam(char *name){
CGIContent *temp=Param->Find(name);
return temp;
};
Стоило бы заметить о некоторых особенностях работы класса на разных платформах. Изначально писал я его в среде Borland C++ 3.1 и в среде Windows все скрипты просто "летали".
При отладке в Linux начались большие проблемы. После поиска ошибок, которых по идее не было, в течении получаса оказалось, что нельзя использовать процедуру free(*char), если
указатель был получен с помощью getenv(). Т.е. я предполагаю в Линуксе процедура getenv() не копирует содержимое переменной окружения в отдельную строку, а возвращает адрес, в котором она содержится.
В Досе таких ошибок не возникало, правда тут нельзя утверждать, что getenv() создает отдельную строку для этого, вероятно процедура free() просто не вызывает того аварийного завершения, которое получается в Линуксе.
Вобщем, что не система, то свои проблемы, да и что вообще там система... Компиляторы чего только стоят :-) Отладка CGI написанных на языке С++ всегда будет иметь некоторую зависимость от платформы или реализации компилятора, хотя считается, что все везде работает по одному алгоритму. Оказывается, что не все и не всегда.
Шаг 16 - Графика на лету это просто.
Не буду продолжать рассказ о классе CGIApp, потому что считаю там должно быть все ясно. У него конечно еще много недочетов, и вероятно будут проявляться глюки, но всеже это лучше чем раньше.
Единственное, что там еще надо доработать это разбор данных из строки QUERY_STRING при обращении методом POST, а то получается, что данные просто хранятся в переменной класса и не разобраны в приемлимый вид. Так что работайте...
Сейчас думаю многим будет интересна возможность создания графики на "лету". Наверняка тысячи раз видели графики на сайтах, диаграммы, таблички и т.д. Все это не рисуют бедные вебмастера каждый час или даже минуту :-)
Вывод графики интересная скажу вам штука... Особенно когда эта операция должна выполняться быстро. Пару лет назад я крепко этим занимался и целыми днями сидел за оптимизацией ассемблерного кода. Иногда уменьшив размер процедуры на пару байт (например, заменив add ax,1 на inc ax) скорость возрастала на 50 и больше процентов. Странно было, но очень приятно... Сейчас уже никто не думает о скоростях и оптимизации, пишут на 500Мб то, что можно "упаковать" в 100 :-))
Вобщем хватит философии, приступим...
При создании картинки монитора может и не быть под рукой, мало того в компьютере может попросту не быть видео адаптера... Так, что все операции надо проводить в памяти на так называемом "виртуальном экране".
Чтобы зря не тратить память лучше при инициализации приложения выделить память под виртуальный экран ровно столько, сколько потребуется для создания картинки. Для начала опишем "болванку" класса, который потом будем постоянно наращивать.
class CGIScreen {
protected:
int width, height;
char *scr;
public:
CGIScreen(int w,int h);
~CGIScreen();
};
Переменные width и height хранят размеры виртуального экрана, которые необходимы для процедур вывода, чтобы отрезать все ненужные или по другому не видимые пикселы.
Указатель scr это и есть тот самый экран. Теперь конструктор и деструктор:
CGIScreen::CGIScreen(int w,int h)
{
if (w*h<=64000)
scr=(char *)malloc(w*h);
else {
scr=NULL;
return;
}
width=w;
height=h;
};
CGIScreen::~CGIScreen()
{
if (scr!=NULL) free(scr);
};
В конструкторе я не стал выделять много памяти. Во-первых, 64000=320*200 чего вполне хватит, чтобы создать картинку немеренного размера (в байтах :-) и разозлить посетителя :-). Во-вторых, я не могу предугадать в каких системах будет использоваться этот код. В ДОСе максимальный блок памяти 64Кб, ну а если у Вас возможно хранить больше, то меняйте код сами :-)
Следующей думаю будет процедура заполнения "экрана" одним цветом. Догадались как это можно быстро сделать ?
int CGIScreen::FillScreen(char color)
{
if (scr!=NULL) setmem((void *)scr,width*height,color);
};
Теперь не забудьте вставить заголовок процедуры в описание класса и можно начинать работать :-)
Будете получать виртуальные экранчики заполненные одним цветом. Помоему не плохо для начала... Подойдет для создания фонов :-)
Шаг 17 - Вывод примитивов на экран.
Экран у нас теперь готов, научимся выводить примитивы на этот экран. Самым "примитивным" из примитивов является точка или пиксел, как кто хочет. Если Вы никогда не занимались графикой, то Вам вероятно будет сложно сообразить, что значит вывести точку.
Видео память в PC-совместимых системах линейна, если не считать древних режимов EGA или CGA. Поэтому для того, чтобы вывести точку на экран достаточно просто записать байт-цвет в соответствующую ячейку в памяти. "Линейная память" надо понимать так: каждая последующая строка пикселов размещается в памяти точно за предыдущей строчкой. Небольшой пример:
RRRGGGGBBB
GGGBBBRRRB
..........
Допустим это картинка на экране, тут буквы RBG это какие-то цвета. Так вот в нашей линейной памяти эти строчки будут располагаться так:
RRRGGGGBBBGGGBBBRRRB..........
Думаю Вам уже должна быть понятна следующая формула: адрес_ячейки=Y_точки * Ширину_Экрана + Х_точки. Именно по ней мы будем вычислять адрес пиксела в памяти. Так вот давайте напишем процедуру вывода этого пиксела:
int CGIScreen::PutPixel(int x,int y,char color)
{
if ((x<0)||(x>=width)||(y<0)||(y>=width))
return -1;
*(scr+y*width+x)=color;
};
Естественно надо не забывать, что точка может заходить за границы экрана, поэтому ее надо проверять. Это делает оператор if (...). Теперь Вы можете выводить точку в любое место и все, что надо будет отображаться.
Дальнейней процедурой должна быть линия. Алгоритмов вычерчивания линии насколько я знаю есть предостаточно, но повсюду используется один - это алгоритм Брезенхейма.
Объяснить по нормальному это дело я Вам сейчас без подготовки не смогу, да и не та у нас тут тематика, по этому просто принимаем "as is" :-)
int CGIScreen::Line(int x1,int y1,int x2,int y2, unsigned char Color)
{
int dx=abs(x2-x1);
int dy=abs(y2-y1);
int sx=x2 >= x1 ? 1:-1;
int sy=y2 >= y1 ? 1:-1;
int d,d1,d2,x,y,i;
if (dy<=dx)
{ d=(dy << 1)-dx;
d1=dy << 1;
d2=(dy - dx) << 1;
PutPixel(x1,y1,Color);
for (x=x1+sx,y=y1,i=1;i<=dx;i++,x+=sx)
{
if (d>0){d+=d2;y+=sy;}
else d+=d1;
PutPixel(x,y,Color);
}
}
else
{
d=(dx << 1) - dy;
d1=dx << 1;
d2=(dx - dy) << 1;
PutPixel(x1,y1,Color);
for (x=x1,y=y1+sy,i=1;i<=dy;i++,y+=sy)
{
if (d>0){d+=d2; x+=sx;}
else d+=d1;
PutPixel(x,y,Color);
}
}
return 0;
};/*Line*/
Вычерчивание линии происходит по точкам, а значит будет действовать наш механизм "отсечки" ненужных пикселов.
Помоему уже не плохо. Самые главные можно сказать примитивы мы научились строить. Дело за немногим, использовать их для создания более сложных объектов.
Шаг 18 - Вывод примитивов на экран (часть 2).
Еще остался один примитив, который является одним из главных. Это естественно окружность, а как примитив с расширенными возможностями лучше сразу рассмотреть эллипс.
Над прорисовкой эллипса тоже постарался Брезенхейм и облегчил задачу всем программистам до сегодняшних дней.
Вот код рисования эллипса:
int CGIScreen::Ellipse(int exc, int eyc, int ea, int eb , unsigned char Color)
{
int elx, ely;
long aa, aa2, bb, bb2, d, dx, dy;
elx = 0; ely = eb; aa = (long)ea * ea; aa2 = 2 * aa;
bb = (long)eb * eb; bb2 = 2 * bb;
d = bb - aa * eb + aa/4; dx = 0; dy = aa2 * eb;
PutPixel(exc, eyc - ely, Color); PutPixel(exc, eyc + ely, Color);
PutPixel(exc - ea, eyc, Color); PutPixel(exc + ea, eyc, Color);
while (dx < dy)
{
if (d > 0) { ely--; dy-=aa2; d-=dy;}
elx++; dx+=bb2; d+=bb+dx;
PutPixel(exc + elx, eyc + ely, Color);
PutPixel(exc - elx, eyc + ely, Color);
PutPixel(exc + elx, eyc - ely, Color);
PutPixel(exc - elx, eyc - ely, Color);
};
d+=(3 * (aa - bb)/2 - (dx + dy))/2;
while (ely > 0)
{
if (d < 0) {elx++; dx+=bb2; d+=bb + dx;}
ely--; dy-=aa2; d+=aa - dy;
PutPixel(exc + elx, eyc + ely, Color);
PutPixel(exc - elx, eyc + ely, Color);
PutPixel(exc + elx, eyc - ely, Color);
PutPixel(exc - elx, eyc - ely, Color);
};
return 0;
};/*Ellipse*/
Здесь координатами центральной точки эллипса являются параметры exc,eyc. Параметры ea,eb это ширина и высота эллипса, подставив вместо них одинаковые значения Вы сможете нарисовать окружность.
Теперь еще два более крупных примитива состоящих из линий. Первый это прямоугольник:
int CGIScreen::Rectangle(int x1,int y1,int x2,int y2, unsigned char color)
{
Line(x1,y1,x2,y1,color);
Line(x1,y1,x1,y2,color);
Line(x2,y1,x2,y2,color);
Line(x1,y2,x2,y2,color);
}/*Rectangle*/
и закращенный прямоугольник:
int CGIScreen::FillRectangle(int X1,int Y1,int X2,int Y2, unsigned char Color)
{
int I;
if (Y1>Y2) {I=Y1;Y1=Y2;Y2=I;};
for (I=Y1;I<=Y2;I++) line(X1,I,X2,I,Color);
}/*FillRectangle*/
Теперь на этой базе вы сможете строить более сложные объекты.
Шаг 19 - Вывод спрайтов на экран.
Эта возможность должна всем очень понравиться, т.к. наверняка кто-то из Вас захочет кроме вывода простых линий вывести какую-то картинку нарисованную в Фотошопе :-)
Естественно запрограммировать вывод этой картинки попиксельно в программе очень сложно. Тут приходят на помощь спрайты. Спрайтами принято называть графические объекты(кусочки изображения) помещенные в память или в файл на диске. Для тех кто слабо себе представляет понятие спрайт поясню: в играх, например, это любой человечек, цветочек, стена и т.д. Когда много спрайтов одного и того же объекта(но с изменениями) быстро выводят на экран получается анимация. Но нам анимация естественно не пригодится :-)
Физически спрайт в наших понятиях это такой же экран, но меньшего размера и заполненный информацией. Поэтому для него также справедливо понятие линейности хранения в памяти.
Вывод спрайта на экран состоит в том, чтобы выделить из последовательности хранящихся в памяти байтов полоску шириной равной ширине спрайта и вывести ее в соответствующее положение на экране. Выделение таких полосок(строк изображения) происходит начиная с первой и до конечной, при этом на экране по одной строчке появляется картинка. Для осуществления такой вот операции "копирования" полоски байтов на экран предназначается следующая процедура:
void MoveLineOfImage(char *Source,char *Dest,unsigned int Count,char Bol)
{
for (int i=0; i<Count; i++){
if ((*Source)!=0)
*Dest=*Source;
else {
if (Bol!=1)
*Dest=*Source;
};
Source++;
Dest++;
};
}
Здесь Source - указатель на источник в памяти, Dest - указатель на получателя, Count - количество копируемых байт.
Признак Bol - это признак копирования нулевых байтов. Если этот признак установлен в 1, то процедура не копирует нулевые байты, при этом не стирается изображение, которое уже было на экране, т.е. достигается эффект прозрачности областей спрайта, которые состоят из нулей.
Теперь на базе этой процедуры можно строить процедуру, которая будет выводить спрайт на виртуальный экран. Разработка данной процедуры и доведение ее до окончательного нормального вида заняла у меня в свое время пару месяцев. Сейчас я Вам привожу код этой процедуры, но только пришлось убрать все ассемблерные вставки (над которыми я так долго работал) на их менее быстродействующие аналоги языка C++. Рассказывать о выводе спрайта не буду, так как сам доходил до этого долго и не та у нас тематика.
void CGIScreen::PutImage(char *p,int x,int y,int bol)
{
int xs = *p; //ширина изображения
int ys = *(p+2); //высота изобажения
int lsx;
if ((x>width) || (y>height) || (x+xs<=0) || (y+ys<=0)) return;
int x1,y1,x2,y2;
x1=y1=0;
x2=xs;
y2=ys;
if (x<0) x1=-x;
if (y<0) y1=-y;
if (x+xs>width) x2=width-x;
if (y+ys>height) y2=height-y;
lsx=x2-x1;
char *p1=p+4+y1*xs+x1;
int i;
long Addr=(y+y1)*width+x+x1;
for (i=y1;i<y2;i++)
{
MoveLineOfImage(p1,scr+Addr,lsx,bol);
p1+=xs;
Addr+=width;
}
};/*PutImage*/
Указатель *p указывает на область памяти, в которой содержится спрайт. Координаты вывода задаются параметрами x,y причем они могут быть меньше нуля. Если при этом кусок спрайта попадает на экран, то будет выводиться именно этот кусок. Признак bol указывает на прозрачность спрайта и используется процедурой копирования строк изображения описанной выше.
Осталось только сказать о формате хранения спрайта в памяти. Как некоторые наверно уже успели заметить кроме самого содержимого спрайта в нем еще содержатся его физические размеры. Эти размеры записаны в самом начале спрайта и занимают 4 байта, т.е. два числа типа int - горизонтальный и вертикальный размер. Естественно формат спрайтов не подчиняется ни одному стандартному формату графических данных и при загрузке спрайта надо обеспечить его преобразование во внутренний формат. В будущем для облегчения работы мы напишем процедуру считывания спрайтов из файлов какого-нибудь типа, например, как самый легкий возьмем BMP.
Но для этого надо еще обеспечить работу с палитрами. Скорее всего этим займемся в дальнейших шагах.
Шаг 20 - Внутренняя палитра.
Для будущей работы с изображениями нам потребуется палитра цветов. До сих пор мы на экран выводили просто номера цветов. Но сам номер цвета без своих основных параметров не несет в себе полезной информации. Поэтому давайте введем массив для палитры.
class CGIScreen{
...
unsigned char Palette[768];
...
};
Сегодня редко требуется цветов для графики больше 256, не считая конечно фотографии. Практически весь Web пытается обходиться 32-64 цветами. Кроме того основные распространенные палитровые форматы графических изображений, такие как GIF, BMP не могут содержать в себе больше 256 цветов.
Поэтому размер палитры равен 256*3, где число три - это количество основных составляющих цвета: красный, зеленый и синий. В мире принят формат хранения цветов в формате RGB, т.е. сначала красный, потом зеленый и синий. Мы будем использовать тот же формат, потому что вероятнее всего в будущем он поможет избавиться от преобразования формата палитры.
Для установки цветов палитры создадим следующую процедуру:
void CGIScreen::SetPalette(unsigned char Color,unsigned char Red,
unsigned char Green,unsigned char Blue){
Palette[Color*3]=Red;
Palette[Color*3+1]=Green;
Palette[Color*3+2]=Blue;
};
Здесь Color - это номер цвета в палитре, а Red, Green, Blue - соответствующие составляющие цвета.
Для облегчения работы с палитрой также нужна функция, которая будет получать цвет из палитры:
void CGIScreen::GetPalette(unsigned char Color,unsigned char *Red,
unsigned char *Green,unsigned char *Blue){
*Red=Palette[Color*3];
*Green=Palette[Color*3+1];
*Blue=Palette[Color*3+2];
};
Теперь давайте договоримся всегда перед рисованием сначала инициализировать соответствующий цвет в палитре.
И старайтесь, чтобы все используемые цвета шли последовательно и их использовалось как можно меньше.
Шаг 21 - Класс шрифтов CGIFont.
Вроде все научились выводить, а вот шрифты еще пока нет. Давайте создадим новый класс для шрифтов. Или Вы их хотите сами рисовать попиксельно ?!
Думаю не надо объяснять, что существует два вида шрифтов: векторные и растровые. Векторные шрифты не плохи (даже круты :-), но на данном этапе лично мне сложно написать за час класс выводящий шрифты Windows TTF или хотя бы Borland CHR. Так что давайте не будем мучиться и разработаем класс для работы с растровыми шрифтами.
Формат наших шрифтов будет чуть похож на формат спрайтов, описанных ранее. Вообще-то видимо я не с этого начинаю :-) Давайте разберемся как мы вообще будем представлять шрифты в памяти.
Для начала надо выбрать размер шрифтов. Горизонтальный размер символа в этом конкретном случае не будет превышать 8 пикселов. По высоте он не ограничен. С чем это связано ? С тем, что мы будем хранить в памяти битовую маску символов. Т.е. если бит в байте равен единице, значит в соответствующем положении на экране будет выведен пиксел. Пример символа "A":
...oo... 0x18
..oooo.. 0x3C
.oo..oo. 0x66
oo....oo 0xC3
oooooooo 0xFF
oo....oo 0xC3
oo....oo 0xC3
В памяти этот символ будет представлен в виде массива байтов:
{0x18,0x3C,0x66,0xC3,0xFF,0xC3,0xC3}
Как видите при таком способе хранения максимальная ширина символа может быть 8 пикселов, а вертикальный размер не ограничен. В принципе никто не мешает нам расширить нашу битовую матрицу до 16 бит, но я думаю символы такой величины не понадобятся.
В памяти будем представлять шрифт таким способом:
1 байт - Ширина символа
2 байт - Высота символа
3 байт - Первая битовая маска первого символа
.....
Если коротко, то первые два байта это физические размеры символов шрифта, а далее битовые образы всех 256 символов. У меня на компьютере имеется несколько интересных шрифтов такого формата. Но для вас напишем утилитку, которая сможет генерить такие шрифты, хотя многие из Вас смогут написать и сами.
Теперь описание класса CGIFont:
class CGIFont{
char *Font;
public:
CGIFont(char *FileName);
~CGIFont();
void WriteChar(CGIScreen *scr,int x,int y, unsigned char color,
unsigned char color1,
int bol,unsigned char ch);
void WriteString(CGIScreen *scr,int x,int y, unsigned char color,
unsigned char color1,
int bol,unsigned char *S);
};
Конструктор будет сразу инициализировать шрифт и загружать его из файла с именем FileName:
CGIFont::CGIFont(char *FileName){
FILE *f;
unsigned char l,k;
long len;
Font=NULL;
if ((f = fopen(FileName, "rb")) == NULL) return;
fread(&k,1,1,f);
fread(&l,1,1,f);
rewind(f);
len=l*256+2;
if((Font=(char *)malloc(len))==NULL){
fclose(f);
return;
};
fread(Font,len,1,f);
fclose(f);
};
Деструктор, можно сказать, в старом стиле:
CGIFont::~CGIFont(){
if (Font!=NULL) free(Font);
};
Теперь осталась процедура вывода символа и строки.
void CGIFont::WriteChar(CGIScreen *scr,int x,int y, unsigned char color,
unsigned char color1,
int bol,unsigned char ch)
{
unsigned char i,j,Number,N;
if (Font==NULL) return;
char l=*Font;
char k=*(Font+1);
for (j=0;j<=k-1;j++)
{
Number=*(Font+2+ch*k+j);
for (i=0;i<=l-1;i++)
{
N = Number << i;
N >>= 7;
if (N==1) scr->PutPixel(x+i,y+j,color);
else if (bol==0) scr->PutPixel(x+i,y+j,color1);
}
}
};
Здесь scr - это тот виртуальный экран, на который нам надо вывести символ. Позиция для вывода символа задается координатами x,y. Цвет символов задается параметром color, а фон цветом color1. При этом, если признак bol равен 1, то фон не выводится. Этот эффект похож на прозрачность в спрайтах. Параметр ch задает тот символ, который надо вывести.
Теперь на базе этой процедуры строим процедуру вывода строки на экран:
void CGIFont::WriteString(CGIScreen *scr,int x,int y, unsigned char color,
unsigned char color1,
int bol,unsigned char *S)
{
unsigned char l;
if (Font==NULL) return;
l=*Font;
int i=0;
while (*S!=\'\\0\')
{
if (*S!=\'\\n\')
{
WriteChar(scr,x+(i*l),y,color,color1,bol,*S);
i++;
}
else {i=0;y+=*(Font+1);}
S++;
};
};
Все параметры аналогичны предыдущей процедуре. Строка задается указателем *S.
Теперь можно выводить символы на виртуальный экран. Графические возможности нашего CGI приложения постоянно растут :-)
Шаг 22 - Скрипт и базы данных.
Такое громкое название, как "База данных" зачастую скрывает в себе лишь один или несколько файлов, в которых хранятся в определенном формате данные. Каждый способен придумать свой формат файла базы данных, но в компьютерном мире уже существуют общепринятые форматы баз данных, которые понимают многие СУБД и программы.
Одним из таких форматов является DBF (Data Base File). Наша задача проста: разобраться с форматом файлов DBF и написать соответствующую библиотеку классов для работы с ними. Я конечно понимаю, что это уже старо как мир и существует много готовых библиотек, но давайте напишем что-то сами для развития мозгов и своего навыка программирования.
Первым делом надо разобраться с форматом файлов DBF. На данном этапе я считаю важным разобраться только с форматом записи файла, а если быть точным, то с форматом описания структуры этой записи.
Вкратце и упрощенно о формате файла DBF. Сначала идет заголовок файла, с ним мы познакомимся чуть позже. Далее идет список полей в записи файла, т.е. имя, тип, длина соответствующего поля в записи. После этого списка идут подряд все записи файла, которые организуются по структуре описанной в списке полей.
Для полного описания поля записи используется структура длинной 32 байта, в которой:
0..10 байт - Название поля (11 байт).
11 байт - Тип поля.
12..15 байт - Позиция поля в записи (4 байта).
16 байт - Общая длина поля в байтах (вкл. плавающую точку).
17 байт - Длина дробной части вещественного числа.
18..31 байт - Зарезервировано (14 байт).
Давайте для хранения этой структуры создадим класс DBFRecordTypeField:
class DBFRecordType;//класс для структуры записи
class DBFRecordTypeField {
friend DBFRecordType;
DBFRecordTypeField *Next, *Pred;//Переменные для списка
public:
char Name[11]; //имя поля
char Type; //тип поля
long Position; //Позиция поля в записи
unsigned char TotalLen;//Длина поля
unsigned char DecimalLen;//Длина дробной части
DBFRecordTypeField(char *name_,char type_,
unsigned char len_, unsigned char declen_);
~DBFRecordTypeField();
};
Этот класс может в себе практически ничего не содержать кроме конструктора и деструктора:
DBFRecordTypeField::DBFRecordTypeField(char *name_,
char type_, unsigned char len_, unsigned char declen_)
{
int i=0;
char b=0;
for (i=0;i<11;i++){
if ((*(name_+i)!=0)&&(b==0))
Name[i]=*(name_+i);
else {
Name[i]=0;
b=1;
};
};
Type=type_;
TotalLen=len_;
if ((Type==\'F\')||(Type==\'N\')){
DecimalLen=declen_;
} else {
DecimalLen=0;
};
if (Type==\'L\') TotalLen=1;
Next=NULL;
Pred=NULL;
};// DBFRecordTypeField;
Имя структуры заполняется слева направо и оставшиеся непонадобившиеся символы заполняются нулями. Переменную DecimalLen заполняем только в случае если поле вещественного или числового типа. А типы полей задаются символами соответствующих следующей табличке.
Тип поля | Значение
|
---|
Числовое | N
|
Вещественное | F
|
Символьное | C
|
Логическое | L
|
Типа MEMO | M
|
Дата | D
|
Теперь осталось написать деструктор, который восстанавливает связи в нашем двусвязном списке. Почему двусвязном ? А просто так, мало ли что потом понадобится :-)
DBFRecordTypeField::~DBFRecordTypeField(){
if (Pred!=NULL) Pred->Next=Next;
if (Next!=NULL) Next->Pred=Pred;
};//~DBFRecordType;
Теперь есть элементарная "базовая ячейка", с помощью которой будем строить класс списка DBFRecordType, хранящего список полей записи.
Шаг 23 - Строим класс DBFRecordType.
Вы уже должны быть знакомы с технологией построения двусвязных списков, хотя бы из предыдущих шагов или из раздела про теорию. Поэтому не буду подробно рассказывать о управляющем классе.
Как и любой другой он должен содержать конструктор создающий список и деструктор правильно удаляющий список из памяти, и конечно процедуры и функции для добавления элемента, поиска и т.д.
Описание класса
class DBFRecordType {
DBFRecordTypeField *List;
public:
DBFRecordType();
~DBFRecordType();
void Add(DBFRecordTypeField *New);
DBFRecordTypeField *FindField(char *name_);
unsigned int Length();
};
Конструктор черезвычайно прост :-)
DBFRecordType::DBFRecordType(){
List=NULL;
};
Деструктор также практически стандартный. Интересно сколькими способами можно осуществить его удаление :-) При рисовании этого деструктора у меня вертелось в голове два и я выбрал наиболее простой.
DBFRecordType::~DBFRecordType(){
DBFRecordTypeField *temp;
temp=List->Next;
while (List!=NULL){
delete List;
List=temp;
temp=temp->Next;
};
};
Теперь процедура добавления элемента. В данном случае у нас получится список типа очередь.
void DBFRecordType::Add(DBFRecordTypeField *New){
DBFRecordTypeField *temp=List;
if (List==NULL){
List=New;
New->Position=1;
return;
};
while (temp->Next!=NULL) {
temp=temp->Next;
};
temp->Next=New;
New->Pred=temp;
New->Position=temp->Position+temp->TotalLen;
};
Прочитав данную процедурку Вы должны были заметить строчки для подсчета позиции поля в записи. Это считаю хорошее свойство файлов DBF, которое позволяет достаточно быстро найти требуемую запись и поле в ней.
Теперь процедурка обеспечивающая поиск в списке поля c именем name_.
DBFRecordTypeField *DBFRecordType::FindField(char *name_){
DBFRecordTypeField *temp;
temp=List;
while (temp!=NULL){
if (stricmp(temp->Name,name_)==0){
return temp;
};
temp=temp->Next;
};
return NULL;
};
Эта функция подсчитывает общий размер записи в байтах. Она потребуется нам в дальнейшем.
unsigned int DBFRecordType::Length(){
DBFRecordTypeField *temp;
unsigned int c=0;
temp=List;
while (temp!=NULL){
c+=temp->TotalLen;
temp=temp->Next;
};
return c;
};
Пока пожалуй все. На данном этапе я думаю нам хватит возможностей этого класса, а если что будем наращивать на него новое "мясо" :-).
Шаг 24 - Класс DBFRecord.
После того как мы создали класс DBFRecordType, задающий структуру информационной записи файла базы данных, можно начинать проектировать класс обеспечивающий работу с записями.
Называться он будет DBFRecord:
/////////////////////////////////////////
////
/// DBFRecord Class
//
class DBFRecord {
DBFRecordType *Type;
char *Content;
int Length;
DBFRecordTypeField *Temp;
public:
DBFRecord(DBFRecordType *type_);
~DBFRecord();
void SetField(char *name_,void *var);
void GetField(char *name_,void *var);
void Write(FILE *f_out);
};
Тут я решил не усложнять себе жизнь списками и хранить запись целиком одной строчкой.
Мы смело это можем делать даже из-за того, что размер записи файла DBF не может превышать 64Кб.
Размер одного поля записи не может превышать 256 байт, поэтому будет лучше если мы определим глобальный буфер такого размера для работы:
#define MAX_DBF_FIELD 257
char dbfbuffer[MAX_DBF_FIELD];
char dbfpattern[10];
Сразу объявил еще одну переменную dbfpattern, она понадобится в некоторых процедурах.
Давайте разберемся с конструктором класса. Понятное дело, что для выделения памяти требуется знать структуру записи и ее размер. Эту структуру мы сообщаем в переменной type_. Это значит, что сам класс записи не будет содержать в себе структуру и будет ее брать извне, поэтому перед созданием записи нам придется создавать ее структуру.
DBFRecord::DBFRecord(DBFRecordType *type_){
Content=(char *)malloc(Length=type_->Length());
if (Content!=NULL){
memset(Content,\' \',Length);
};
Temp=NULL;
Type=type_;
};
Обратите внимание, что пустая запись DBF файла состоит из пробелов, т.е. попросто говоря все данные хранятся в текстовом формате.
А теперь наистандартнейший деструктор:
DBFRecord::~DBFRecord(){
if (Content!=NULL) free(Content);
};
Теперь нам остались самые интересные процедуры. Естественно это процедуры записи и считывания данных из полей записи. Первым делом запись :-)
void DBFRecord::SetField(char *name_,void *var){
long l=0;
if (Temp!=NULL){
if (strcmp(Temp->Name,name_)==0) goto cont;
};
Temp=Type->FindField(name_);
if (Temp==NULL) return;
cont:
switch (Temp->Type) {
case \'F\': case \'N\':{
sprintf(dbfpattern,"%%%d.%df",Temp->TotalLen,Temp->DecimalLen);
sprintf(dbfbuffer,dbfpattern,*(float *)var);
memmove(Content+Temp->Position-1,
dbfbuffer+(strlen(dbfbuffer)-Temp->TotalLen),Temp->TotalLen);
break;
};//case \'F\'
case \'C\':{
l=strlen((char*)var);
memset(Content+Temp->Position-1,\' \',Temp->TotalLen);
if (Temp->TotalLen>l)
memmove(Content+Temp->Position-1+Temp->TotalLen-l,var,l);
else
memmove(Content+Temp->Position-1,
(char*)var+l-Temp->TotalLen,Temp->TotalLen);
break;
};//case \'C\'
case \'L\':{
if (*(char *)var==0)
*(Content+Temp->Position-1)=\'F\';
else
*(Content+Temp->Position-1)=\'T\';
break;
};//case \'L\'
};//switch
};
Запись у нас осуществляется в поле с названием name_. Данные не имеют определенного типа, поэтому это указатель типа void. Процедура находит в списке типов полей Type поле с именем name_ и определяет его тип по содержимому переменной DBFRecordTypeField->Type. Далее уже записываем данные по соответствующему формату.
Эта процедура пока не реализует возможность записи полей типа memo и date. Данные поля типа date хранятся в американском формате MM/DD/YY и думаю вам придется самим написать соответствующий кусок кода реализующий работу с датой в вашей конктерной операционной системе.
Теперь процедура считывания работающая по такому же принципу.
void DBFRecord::GetField(char *name_,void *var){
long i,l;
char *t=NULL;
if (Temp!=NULL){
if (strcmp(Temp->Name,name_)==0) goto cont;
};
Temp=Type->FindField(name_);
if (Temp==NULL) return;
cont:
switch (Temp->Type) {
case \'F\': case \'N\':{
memmove(dbfbuffer,Content+Temp->Position-1,Temp->TotalLen);
dbfbuffer[Temp->TotalLen]=0;
*(float *)var=atof(dbfbuffer);
break;
};//case \'F\'
case \'C\':{
t=Content+Temp->Position-1;
l=0;
while ((*t==\' \')&&(l<Temp->TotalLen)){t++; l++;}
i=0;
while (l<Temp->TotalLen){
*((char *)var+i)=*t;
t++;l++;i++;
};
*((char *)var+i+1)=0;
break;
};//case \'C\'
case \'L\':{
if (*(Content+Temp->Position-1)==\'F\')
*(char *)var=0;
else
*(char *)var=1;
break;
};//case \'L\'
};//switch
};
Тут слова можно сказать те же самые, но стоит отметить то, что при возвращении текстовой строки не производится контроль за какими-либо переполнениями, поэтому прежде чем получатьб строковый параметр выделите достаточное количество памяти, а проще всего пользоваться созданным нами ранее глобальным буфером dbfbuffer. Считав в него строку Вы далее можете ее копировать в другие переменные.
Осталась функция вывода записи. Она выводит запись целиком в файл f_out. Потом вероятно мы с ее помощью будем записывать данные в DBF файл. Процедуру считывания записи из файла придумаем позже, когда она понадобится.
void DBFRecord::Write(FILE *f_out){
if (Content!=NULL)
for (long i=0;i<Length;i++)
fprintf(f_out,"%c",*(Content+i));
};
Дальше уже будем думать над классом обеспечивающим работу с самими файлами DBF. Если обнаружите какие-либо глюки в классах, то пишите и будем исправлять.
Шаг 25 - Класс DBFFile.
Дождались венца нашего творения :-) Все что мы делали до сих пор было предназначено именно для этого класса.
Но перед тем как мы приступим к его написанию надо разобраться со структурой файла DBF, т.к. до сих пор мы разобрались только со структурой задания полей записи.
Вобщем все просто, как и везде :-). Заголовок также как и структура задающая поля занимает 32 байта. Содержит следующие данные:
Байт | Что содержит
|
---|
00 | Заголовочный байт. Содержит тип DBF файла:
- FoxBASE+/dBASE III +, без memo - 0x03
- FoxBASE+/dBASE III +, с memo - 0x83
- FoxPro/dBASE IV, без memo - 0x03
- FoxPro с memo - 0xF5
- dBASE IV с memo - 0x8B
|
---|
01-03 | Дата последнего изменения (ГГММДД)
|
---|
04-07 | Число записей в файле (unsigned long )
|
---|
08-09 | Полная длина заголовка (положение первой записи)
|
---|
10-11 | Длина одной записи с данными (+ признак удаления)
|
---|
12-27 | Зарезервировано
|
---|
28 | Есть ли составной индексный файл (типа .CDX)
|
---|
29-31 | Зарезервированы
|
---|
Далее сразу за заголовком следуют блоки по 32 байта описывающие типы полей записи. Они нами рассматривались раньше.
После всего этого дела следует завершающий заголовок байт, значение которого равно 0x01.
После заголовка идут сами записи. Перед каждой записью резервируется один байт для признака удаления записи. Удаленные записи помечаются символом *(звездочка), а не удаленные пробелом (т.е. "пустым" текстовым значением). В конце файла после всех записей записывается байт окончания файла, значение которого 0x1A.
Таким образом мы можем составить класс DBFFile:
/////////////////////////////////////
////
/// DBFFile Class
//
class DBFFile {
char Version;//версия DBF файла
char ModifyDate[3]; //Дата модификации ГГММДД
unsigned long RecordsCount; //количество записей в файле.
unsigned int FullHeaderLen;//полная длина заголовка
//(положение первой записи в файле)
unsigned int RecordLen;//длина записи + признак удаления(1 байт)
DBFRecordType *Struct;//структура записи файла
FILE *DBF;
public:
DBFFile();
~DBFFile();
int Open(char *name_);
int ReadRecord(unsigned long Num, DBFRecord *Buf);
int WriteRecord(unsigned long Num, DBFRecord *Buf);
unsigned long AppendRecord(DBFRecord *Buf);
int CheckRecord(long Num);
DBFRecordType *GetStruct();
};
Конструктор и деструктор класса выглядят следующим образом:
DBFFile::DBFFile(){
RecordsCount=0;
FullHeaderLen=0;
RecordLen=0;
Struct=NULL;
DBF=NULL;
};
DBFFile::~DBFFile(){
if (DBF!=NULL) fclose(DBF);
if (Struct!=NULL) delete Struct;
};
Процедура Open(...) открывает уже существующий файл базы данных, считывает
заголовок файла и строит список структур полей записи из класса DBFRecordTypeField:
int DBFFile::Open(char *name_){
int i;
char type,len,declen;
DBF=fopen(name_,"rb+");
if (DBF==NULL) return -1;
fread(&Version,1,1,DBF);
fread(ModifyDate,1,3,DBF);
fread(&RecordsCount,4,1,DBF);
fread(&FullHeaderLen,2,1,DBF);
fread(&RecordLen,2,1,DBF);
fseek(DBF,32,SEEK_SET);
i=((FullHeaderLen-1)/32)-1;
Struct=new DBFRecordType();
while (i>0){
fread(dbfbuffer,11,1,DBF);
dbfbuffer[11]=0;
fread(&type,1,1,DBF);
fseek(DBF,4,SEEK_CUR);
fread(&len,1,1,DBF);
fread(&declen,1,1,DBF);
Struct->Add(new DBFRecordTypeField(dbfbuffer,type,len,declen));
fseek(DBF,14,SEEK_CUR);
i--;
};
return 0;
};
Думаю комментарии излишни. Все делается строго в соответствии с форматом :-))
Следующая не менее важная функция считывания записи из файла. Для работы данной процедуры требуется уже созданная динамическая переменная Buf класса DBFRecord. При создании этой переменной потребуется воспользоваться структурой записи, которую создает Open(..) на этапе открытия. Num это номер записи файла, которую требуется считать :-)
int DBFFile::ReadRecord(unsigned long Num, DBFRecord *Buf){
char Deleted;
if ((DBF==NULL)||(Num>RecordsCount)) return -1;
fseek(DBF,FullHeaderLen+(RecordLen)*(Num-1),SEEK_SET);
fread(&Deleted,1,1,DBF);
Buf->Read(DBF);
if (Deleted==\' \') return 1; else return 0;
};
Результатом данной функции также является признак удаления этой записи. Если считываемая запись была удалена, то функция возвратит 0, если все нормально, то 1.
int DBFFile::WriteRecord(unsigned long Num, DBFRecord *Buf){
char Deleted=\' \';
if ((DBF==NULL)||(Num>RecordsCount)) return -1;
fseek(DBF,FullHeaderLen+(RecordLen)*(Num-1),SEEK_SET);
fwrite(&Deleted,1,1,DBF);
Buf->Write(DBF);
return 0;
};
Все аналогично... При записи данных сбрасывается признак удаления, т.е. заполняется пробелом.
Для добавления в файл новой записи у нас будет служить процедура AppendRecord(...). Она также обновляет (увеличивает) значение количества записей в соответствующем поле заголовка файла.
unsigned long DBFFile::AppendRecord(DBFRecord *Buf){
if (DBF==NULL) return -1;
char Deleted=\' \';
RecordsCount++;
fseek(DBF,4,SEEK_SET);
fwrite(&RecordsCount,4,1,DBF);
fseek(DBF,FullHeaderLen+(RecordLen)*(RecordsCount-1),SEEK_SET);
fwrite(&Deleted,1,1,DBF);
Buf->Write(DBF);
Deleted=0x1A;
fwrite(&Deleted,1,1,DBF);
fflush(DBF);
return RecordsCount;
};
Все видимо должно быть ясно...
Можно еще тонну всяких процедур придумать, и они у нас еще далеко не все... Пока из таких "вспомогательных" давайте сделаем функцию проверки удаления записи:
int DBFFile::CheckRecord(long Num){
char Deleted;
if ((DBF==NULL)||(Num>RecordsCount)) return -1;
fseek(DBF,FullHeaderLen+(RecordLen)*(Num-1),SEEK_SET);
fread(&Deleted,1,1,DBF);
if (Deleted==\' \') return 1; else return 0;
};
Есть последний момент. При создании переменной DBFRecord Вам как-то придется получить содержимое переменной Struct, а она у нас является приватной. Для этого давайте напишем функцию:
DBFRecordType *DBFFile::GetStruct(){
return Struct;
};
При создании этого класса пришлось чуток изменить класс DBFRecord, а именно добавить или изменить процедуры записи и считывания данных:
void DBFRecord::Write(FILE *f_out){
if (Content!=NULL){
fwrite(Content,1,Length,f_out);
fflush(f_out);
};
};
void DBFRecord::Read(FILE *f_in){
if (Content!=NULL)
fread(Content,1,Length,f_in);
};
Добавьте их в прежний класс.
Пока все. Теперь уже можно работать с базами данных. Осталось лишь немногое, это обеспечение создания нового файла DBF, т.е. создание процедур и функций для построения списка Struct и записи полного заголовка в новый файл. Может сделаем позже, а можно и оставить как "хоум ворк" :-))
Шаг 26 - Передаем разрешение монитора в CGI скрипт.
Пришел вот такой вопрос:
Не могли вы бы подсказать, как передать данные
из javascript програмы в perl (CGI).
Меня в частности интересует вопрос об разрешении
монитора пользователя, но я это могу определить
только с помощью javascript, а как передать эти
данные через CGI на сервер незнаю.
--
Женя Краус
Интересная задачка :-) Встречный вопрос: Вы видели как проделывают такую штуку счетчики spylog или top.list.ru ? Если не видели, то давайте посмотрим.
Первым делом разберемся с тем, как получить это самое разрешение монитора, так как многие наверняка не умеют это делать и тоже хотят научиться. Вот смотрите код:
<script language=javascript1.2>
document.write(screen.width,\'x\'+screen.height);
</script>
Заметьте прежде всего, что получение разрешения монитора возможно только с помощью JavaScript 1.2, т.е. более новой версии.
В данном примере у Вас выведется разрешения монитора, например у меня выводится:
1024x768
Таким образом строку с разрешением мы можем всегда получить. Теперь давайте разберемся с тем, как передавать эти данные в скрипт.
Передать данные можно несколькими путями. Один из таких путей как раз используют вышеупомянутые счетчики. Они передают данные прямо вместе с сылкой, т.е. внутри URL. Если Вы внимательно читали шаги раньше, то знаете, что такие данные передаются методом GET скрипту и хранятся внутри переменной окружения QUERY_STRING. Теперь для примера скрипт, который будет передавать разрешение таким способом.
<script language=javascript1.2>
document.write("<a href=test.cgi?screen=",
screen.width+\'x\'+screen.height,">Click</a>");
</script>
В результате у вас внутрь документа встроится код html:
<a href=test.cgi?screen=1024x768>Click</a>
В данном случае данные будут передаваться только при нажатии на ссылку, поэтому также как и счетчики используйте тег <img ...>.
Второй способ, возможно именно он и нужен, будет передача этих данных через форму. В этом случае надо будет завести скрытый параметр в форме, например обзовем его scr. Выглядеть наша форма будет следующим образом:
<form action=test.cgi method=POST name=form1>
<input type=text name=data>
<input type=hidden name=scr>
<input type=submit>
</form>
Теперь для присвоения этому параметру значения будем использовать следующий скрипт:
<script language=javascript1.2>
document.form1.scr.value=screen.width+\'x\'+screen.height;
</script>
После того, как браузер интерпретирует этот код в скрытом параметре формы окажется разрешение экрана.
Например, вот какие данные отправились из формы при использовании action=mailto:[email protected]:
data=hehehe
scr=1024x768
Помоему это именно то, что нам и нужно... Давайте теперь узнаем, что за железо у наших посетителей :-)))
Шаг 27 - Некоторые ответы.
У многих возникают по началу достаточно много естественных вопросов. Привожу конкретное письмо, думаю ответ будет интересен многим.
Здравствуйте! Пишу вам почти отчаившись создать что-то работающее для web
странички на perl. Перечитал сотни страниц доков, FAQ, пособий и.т.д. но
ничего не помогает.
Все предложенные варианты, программы не работают. Все грузят про CGI.pm а я
не пойму как с ним работать. Уже десять дней ломаю голову как сделать
рабочую web-программу (которая могла бы работать дома без сервера интернет и
на виндах).
Объясните пожалуйста как можно сделать самую примитивную гостевую книгу.
Чтобы из текстовой формы браузера, введенная информация пересылалась на
сервер и записывалась в htm вайл? Что для этого нужно, писать файл *.cgi или
*.pl. Можно ли сделать так, чтобы гостевая книга работала у себя дома на
компе и без интернет сервера. Как избежать у себя на PC сообщения брайзера
сохранить ли *.pl или открыть его на месте.
(комп с WINDOWS 98). Извините за такое количество вопросов,
я буду Вам очень благодарен если Вы ответите хотя бы на некоторые из них.
С уважением, Артем.
Во-первых прекрасно, что ломание головы происходит только 10 дней :-) Во-вторых, все видимо связано с неправильным пониманием механизма всего происходящего.
Для начала надо понять, что CGI это интерфейс работы браузера и веб-сервера. Поэтому очевиден первый ответ: без веб-сервера (или в терминах этого письма - сервера интернет) ничего и никогда работать не будет.
И по этой причине главным условием работы этого механизма является правильно настроенный на работу веб-сервер и браузер.
Механизм CGI предназначен для работы веб-сервера и внешних программ обработчиков. Это значит просто то, что все возможности в веб-сервер встроить нельзя поэтому ему просто необходимо запускать внешние программы.
Понятное дело веб-сервер также не знает на каком диалекте умеет "разговаривать" эта программа, чтобы сообщать и получать данные. Для ликвидирования такой неразберихи был разработан простой общий интерфейс взаимодействия CGI.
Теперь когда есть грамотный сервер и интерфейс можно копаться во внешней программе. Очень важное и лучшее, я считаю, свойство CGI это то, что внешняя программа может быть написана на ЛЮБОМ языке программирования.
Предположим у нас имеется программа написанная на каком-то языке. Прежде всего ее надо сделать запускаемой, т.е. скомпилировать. Если это скриптовая программа, на языке Перл к примеру, то надо обеспечить правильную настройку интерпретатора и настройку веб-сервера. Какое имя дать *.cgi или *.pl будет зависеть только от того, как Вы настроите сервер. Если он будет знать, что все файлы с именами *.pl являются скриптами Перл, то для обработки запроса будет запускать интерпретатор.
Здесь есть еще тот факт, что операционные системы тоже все разные. Если в системе Юникс можно установить на файл атрибут "запускаемый", то в системах Виндовс такое различие происходит только по имени файла, поэтому файлы с расширениями отличными от EXE система никогда просто так не запустит. В этом случае необходимо вмешательство веб-сервера, который должен четко знать, что делать со всеми используемыми файлами.
В случае если веб-серверу неизвестно ничего о типе файла (или о том, что его надо запускать) он попросту предлагает скачать содержимое этого файла браузеру, от того и появляется окно с предложением закачки. А уж если без веб-сервера, то браузер тем более сможет лишь только скачать файл, потому что он неумеет ничего запускать (из соображений безопасности).
Решением всех проблем будет правильная настройка на своем компьютере веб-сервера и интерпретатора. Для того, чтобы не подключаться к интернету надо просто лишь вызывать http://127.0.0.1/ или http://localhost/ тогда браузер не будет лезть в инет, а будет работать на локальном компьютере. А как Вы думали ?! Можно ли без локальной отладки пускать программу в суровый мир сети ?! Конечно нельзя.
Еще на последок хочу отметить, что CGI.pm это достаточно важный модуль для разработки приложений. Если Вы читали все шаги из данного раздела, то Вам не надо объяснять, что работа интерфейса достаточна сложна и не всегда интересно ее реализовывать каждый раз в новой программе. Для упрощения программирования как раз и был создан этот модуль под Перл. Мы же с Вами создавали класс CGIApp для С++ и тем самым тоже несколько облегчили свою жизнь. Читайте прошлые шаги.
Вобщем-то теперь думаю написать гостевую книгу будет намного проще :-) Потом мы уже пробовали это однажды делать и тут вопросов не должно быть. Если Вам слишком сложно писать на Перле, то пишите на Паскале или С++. Весь обмен прост и происходит через обычные функции read и write, т.е. через стандартные потоки ввода/вывода.
Шаг 28 - 500 Internal Server Error.
Если Вы хоть раз натыкались на эту ошибку во время разработки скрипта, то Вы счастливый человек :) Скорее "счастливый"... Эта ошибка прямой показатель Вашего стремления освоить CGI. Если человек не представляет себе о ее существовании, и при этом утверждает, что он "гуру" в этом деле, то готовьте для него корзину тухлых помидоров.
Вода... Водичка... А теперь к делу :) Ошибка эта дословно переводится как "Внутренняя Ошибка Сервера" (знатоки забугорного если что подправят :)
Люди разрабатывающие веб-сервер делают это мно-о-о-го лет, поэтому можно утверждать на 99,9%, что внутри его ошибок нет :) По крайней мере вызывающих такой конкретный отказ в работе, как ошибка 500. Ошибка эта может произойти только в случае некорректной работы вашего скрипта. Насколько некорректной сейчас разберемся...
У меня вопрос: Вы знаете как узнать от чего произошла ошибка ?! Уверен, что 50% новичков (не хочу говорить о профях) скажет что незнает... И правильно скажет, иначе не было бы этого шага :)
Хочу Вас научить один единственный раз откуда получать такую конфиденциальную информацию. Правильно (или Вы еще не сообразили ?), из файла /logs/error.log (или схожего по названию).
Получили мы ошибку 500... Что делать ? Идем в файл error.log и смотрим в последнюю строку... У меня почти всегда одна и таже ошибка, вот примерно такая...
[Sat Oct 21 10:00:00 2000] [error] [client 123.45.67.89] Premature end of script headers: с:/apache/cgi-bin/1.cgi
Заметьте то, что сервер не скрывает от Вас ничего и прямо говорит "Ну типа у меня тут Premature end of script headers, и ваще отстань... 500...". Берем "забугорно-местный" переводчик и получаем нечто похожее на "неожиданный конец заголовка скрипта".
И что ? Мысли есть ? Так вот теперь почему это происходит (во многих случаях). Вы попросту неправильно составили заголовок ответа. Заголовок должен быть отделен от содержимого пустой строкой. Например, вместо:
printf("Content-type: text/html\\n\\n");
Вы написали строку с одной \\n:
printf("Content-type: text/html\\n");
Также результатом этой ошибки может быть (а скорее всего и есть) неправильное окончание программы. Например, когда программа выполняется правильно после завершения она всегда дает знать системе, что она выполнилась на отлично, а в баллах системы на 0 :) После аварийного же завершения программы система не получает ответа равного 0, и поэтому это является прямым признаком ошибки.
В Линуксе есть другая проблема. Если Вы неправильно работаете с динамической памятью, то программа сразу же вываливается с сообщением:
Segmentation fault
А если присмотреться, то данная строка совсем не похожа на "Content-type:". Отсюда ошибка 500 о неправильном заголовке ответа.
Также из личного опыта, но это явление я объяснить не могу... Скрипты конкретно вываливаются и все тут. Десяти разовая проверка не дает результата. И тут приходит мысля о том, чтобы поменять местами участки считывания данных из stdin и вывод в stdout. Например, если Вы сначала считывали, то теперь просто сначала выведите "Content-type: ...\\n\\n", а потом считывайте. Незнаю почему, но иногда помогает. Объяснить ничем не могу, разве только тем, что Апач у меня в Линуксе старенький и видимо у него не все дома в этот момент...
При разработке скрипта на любом языке очень аккуратно подходите к динамической памяти. Неправильно инициализированная переменная приводит к невероятным результатам.
Теперь немного о том, что пишут на эту тему профессионалы (с моим фривольным разбавлением) :
- Ошибка 500 может возникнуть от того, что Вы забыли о пустой строке между HTTP-заголовком и телом (но об этом я сказал).
- Сервер хочет получить доступ к сценарию с правом чтения и выполнения, но не может. Лекарство: установите атрибуты на файл 555 или 755 (это для Unix систем).
- Каталог, в котором находится сценарий также должен быть выполняемым, поэтому должен иметь режим 111, а лучше 755.
- Серверу было сказано, что CGI программа должна быть с именем *.cgi, а сами дают что-то типа *.pl или *.exe. Лекарство сомнительное, но всеже проверьте настройки сервера. Также все сопутствующие .htaccess файлы.
- Конфигурация сервера не позволяет скрипту использовать методы GET или POST. Иногда встречаются такие параноики админы :) Лекарство: топор от головной боли админу.
- Также советуют сбрасывать буфера, или отключать буферизацию потока stdout. Лекарство: см. мануал по своему языку.
Вобщем такие пироги. Некоторым они уже "поперек горла", но как же без них. Самое главное ничего не бояться и везде совать свой нос :)
Шаг 29 - Основные принципы построения защищенных CGI.
Вот у меня возник такой вопрос. Вы очень часто говорите о защите от
атак всяких злых дядек, именующих себя "хакерами". Мой будущий сайт
представляет собой биржу труда и образовательных услуг на WEB
технологиях, следовательно его нужно сделать достаточно устойчивым от
всяких некорректных действий. Если можно, расскажите мне о наиболее
часто применяемых методах хакерских атак, наверняка Вы сталкивались с
ними очень часто. А я попробую учесть Ваш опыт и принять меры по
защите от деструктивных действий. Был бы очень Вам благодарен.
С уважением, Денис.
Вообще "хакер" это человек непонятный. В большинстве случаев это просто "обчитавшийся" хакерских мануалов юзер, которому нечего делать в интернете. Реже встречаются настоящие профи, но вероятность того, что они заинтересуются Вашим проектом невелика, если Вы конечно не веб-мастер сайта Микрософта %)
Сайт могут ломать с несколькими целями. Первой и наиболее простой является получение какой-либо "скрытой" информации с него. Вторая более сложная, ломание сайта как некий этап взлома всей системы или сети. Например, с помощью ваших неграмотных скриптов хакер может получить какие-либо настройки системы для дальнейшего взламывания. Но главное правило построения защищенных копроративных сетей есть вытаскивание веб-сервера в демилитаризованную зону, т.е. размещение его во внешней части корпоративной сети с отделяющим сетевым экраном. Иногда (хотя скорее почти всегда) веб-сервер вообще не подключен к корпоративной сети, а например к провайдеру. Поэтому взлом веб-сервера не может нанести угрозы внутренней сети и хакеру с ним возиться "себя не уважать". В итоге можно с большой уверенностью сказать, что взломом сайтов занимаются для получения "скрытой" информации с него или для крушения системы (например, чтобы сайт не мог функционировать).
Все атаки по основным признакам можно разделить на внутренние и внешние. Внешние атаки идут из сети. Они чаще всего связаны с нарушением функционирования и реже с получением информации (потому как издалека файл сложно прочитать), поэтому их последствия не так страшны (если не учитывать потерь от простоя сайта). Внутренние атаки могут исходить от пользователей сервера и, что самое страшное, от администратора. Эти атаки уже больше связаны с получением информации, чем с нарушением функционирования (т.к. администратору это ничего не стоит %), поэтому могут носить более тяжкие последствия. Пользователи сервера могут без разрешения просматривать Ваши данные или даже исходники (если скрипт на Перле).
Сейчас мы разобрались только с теми, кто нас может атаковать. Теперь давайте разберемся с методами "защиты".
1. Недоверяй никому.
Самый главный принцип (и не только защиты CGI :). Заключается в проверке любых данных получаемых скриптом.
Обязательно надо проверять содержимое на наличие запрещенных символов и их последовательностей. Например, почтовый адрес должен обязательно содержать @ и хоть одну точку. Если этих признаков нет, то адрес введен не верный и его нельзя использовать.
Надо осуществлять проверку на допустимую длинну. Опять применительно к почтовому адресу, если Вам передали адрес длинной больше 100 символов, то это очень странно. Лучше попросить ввести другой и покороче :)
Если Вы используете элементы <select>, то проверяйте действительно ли содержимое данного поля принадлежит набору допустимых значений.
Если полученные данные содержат имя файла или системную команду, то проверяйте куда ведет путь и что может вытворить команда.
2. Размещай скрипты в специальном месте.
Это большей частью относится к пользователям Unix систем. Никогда не храните скрипты в каталогах с открытым на запись доступом. Ваш скрипт могут перезаписать или удалить другие пользователи сервера. Создавайте для своих скриптов отдельные каталоги с запретом на запись. При этом не забывайте сделать правильные настроками веб-сервера в этом каталоге, чтобы сервер мог использовать скрипты.
3. Защищайте все секретные данные.
Конечно же совершенно секретные данные на веб-сервер размещать нельзя. Защищать надо все, что носит хоть какой-то признак приватности. Например, к приватным данным можно отнести пароли, номера кредитных карт, почтовые адреса пользователей, их имена и т.д. Когда я рассказывал про внутренние атаки я имел в виду именно этот тип данных. Если Вы скопили достаточно большой список пользователей с их почтовыми адресами, то он может служить объектом атак внутренних пользователей. Не секрет, что можно продавать списки адресов спамерам или пользователям, которые любят регистрироваться от чужого имени. Про пароли и номера кредитных карт я вообще молчу (их уже можно отнести к секретным). Зная такие данные можно творить вообще, что душе угодно. Из-за этого не надо надеяться на честность админа или пользователей. Человеческий фактор самый страшный, если машина предать не способна, то человеку это "раз плюнуть".
Поэтому при записи приватных данных на локальные диски или в базы данных все шифруйте. Давайте именам файлов непонятные названия, например, вместо password.dat назовите файл colors.dat или 12pqe23.dat %) Такие имена не привлекают внимание и при условиях шифровки внутренностей будут считаться простым мусором %)
Также при использовании Cookies не забывайте, что все данные, которые Вы отправили клиенту может просмотреть любая программа. Например, дырявый браузер может вызвать утечку секретных данных, таких как пароль, логин и т.д. Передавайте все такие данные в зашифрованном виде. Браузеру все равно, что он записывает, а Вам не все равно. Помните, что каждый перехваченный пароль увеличивает возможность проникновения в Ваш скрипт (или систему, в которой он работает).
Особенно если Ваш скрипт обеспечивает удаленное управление системой (например, банерообменником, гостевой книгой и т.д.). Человек с повышенными правами может Вам все попортить...
4. Сделайте систему отчета.
Система отчета сильно Вам поможет при обнаружении ошибок или попытках взлома. Записывайте в лог-файл все нестандартные действия. При этом сохраняйте значения:
- Время запуска программы.
- Значения, которые передал пользователь.
- Название Хоста и IP адреса.
В принципе можно ограничиться записыванием только нестандартных действий, но на первое время (время отладки) записывайте все обращения к скрипту.
Возможно Ваш скрипт кто-то захочет обдурить, и пусть даже у него это получится, зато при полном отчете Вы будете знать как он это сделал и сможете убрать "дырки".
5. Подумайте о многопользовательском режиме.
Обязательно на этапе разработки встраивайте код, который будет каким-либо образом ограничивать работу скрипта, если система уже запустила этот скрипт раньше. Например, при записи данных в файл делайте блокировку файла на запись для других программ или вводите какой-то специальный признак, чтобы другие скрипты знали про блокировку.
6. Одна голова хорошо, а две лучше.
Дайте скрипт на проверку своему компетентному другу. Или даже разработайте скрипт с ним вместе. Таким образом Вы избежите большого количества ошибок.
Прежде чем "вывесить" скрипт в сеть проверьте его со всех сторон. Станьте на время взломщиком своего скрипта и оцените его устойчивость со всех сторон. Проанализируйте работу скрипта при всевозможных неправильных данных (хорошим помощником будет двух летний брат за клавиатурой %)