Глава 7. FTP-серверДжон ПенманКак известно, в FTP участвуют две стороны. Создание нестандартного компонента, выполняющего функции FTP-сервера, позволит вам полностью контролировать операции пересылки файлов между Internet-приложениями. В главе 6 я описал компонент CsShopper, в котором инкапсулируются функции клиентской стороны при пересылке файлов с использованием протокола FTP. Более того, компонент, выполняющий функции FTP-клиента, даже входит в число примеров Delphi 3. И все же для осуществления полноценного обмена файлами недостаточно иметь только клиентское приложение. Сейчас в Сети появляется все больше пользователей с круглосуточным доступом (за которым закрепился термин 24?7), и все больше людей желает создавать на Delphi свои собственные программы-серверы. Итак, знакомьтесь — CsKeeper! CsKeeper — потомок компонента CsSocket из главы 5. В этом VCL-компоненте инкапсулируется серверная сторона FTP-протокола. CsKeeper чем-то похож на продавца маленького магазинчика — он «берет с полки» те файлы, которые затребованы, и передает их клиенту «через прилавок». Впрочем, в отличие от продавца сервер является конечным автоматом, строго соблюдающим правила протокола FTP (и к тому же не пытается болтать на посторонние темы). Большая часть того, что было сказано о компоненте CsShopper в главе 6, относится и к CsKeeper. Если вы еще не читали главу 6, я настоятельно вам рекомендую начать именно с нее. В сложном танце под аккомпанемент FTP-протокола участвуют две стороны, и понимание одной из них невозможно без определенного понимания другой. Если вы считаете, что достаточно хорошо разобрались с клиентской стороной, мы можем продолжать. Сервер FTP обычно ожидает установки клиентского соединения на TCP-порте с номером 21. При соединении сервер инициирует процесс регистрации, посылая клиенту команду USER. Поскольку процесс регистрации был достаточно подробно рассмотрен в главе 6 при описании CsShopper, я не стану задерживаться на его подробностях. После успешной регистрации сервер готов к выполнению любого FTP-запроса, поступившего от клиента. Магазин открылся! К тому что происходит дальше, стоит присмотреться повнимательнее. В компоненте CsKeeper воплощен простой и полезный FTP-сервер, который соответствует минимальным требованиям, формально изложенным в документе RFC959. Следовательно, некоторые команды FTP (такие как ACCT, NLIST и PASV) в настоящее время отсутствуют в словаре CsKeeper. В таблице 7.1 приведен список всех FTP-команд. Команды, не реализованные в текущей версии CsKeeper, помечены звездочкой. При получении неподдерживаемой команды CsKeeper возвращает клиенту код ошибки с содержательным сообщением. Обратите внимание: CsKeeper не является FTP-сервером с параллельной обработкой. Это означает, что в каждый момент времени он может обслужи вать лишь одного пользователя. Таблица 7.1. Набор команд FTP
CsKeeper за работойПриложение KEEPER32 (находится на CD-ROM в каталоге этой главы) показывает, как компонент CsKeeper используется в приложении. Форма приложения содержит три элемента-вкладки (TabSheet). Вся основная работа выполняется на первой вкладке, tsKeeper (см. рис. 7.1). Также присутствуют вкладки tsOptions и tsAbout (о них будет рассказано ниже).
Рис. 7.1. KEEPER32 в режиме конструирования (отображается вкладка tsKeeper) Но перед тем, как запускать приложение KEEPER32, необходимо выполнить некоторые подготовительные действия. Конечно, можно определить поведение компонента CsKeeper1, изменяя значения его свойств в инспекторе объектов в режиме конструирования (см. рис. 7.2). Однако работа со свойствами в режиме конструирования удобна для разработчика приложения, но никак не для пользователя — например FTP-администратора, который может вообще не быть программистом и не иметь доступа к исходным текстам программы и к среде Delphi. Администратор наверняка предпочтет работать с информацией о конфигурации FTP-сервера на вкладке tsOptions (обратите внимание: любые изменения в конфигурации учитываются только при загрузке и запуске приложения, поэтому, чтобы они подействовали, придется перезапустить FTP-сервер). Эта вкладка показана на рис. 7.3.
Рис. 7.2. Свойства CsKeeper1 в инспекторе объектов
Рис. 7.3. Вкладка Options в режиме конструирования Конфигурирование KEEPER32 на вкладке OptionsНа этой вкладке сосредоточено множество полезных функций. Прежде всегос ее помощью можно предотвратить «блуждание» клиентов по файловой системе сервера. Мы можем ограничить доступ FTP-клиентов определенным диском и основным каталогом того компьютера, на котором выполняется KEEPER32. Следовательно, FTP-клиент не сможет выйти за пределы каталога, указанного в свойстве CsKeeper1.RootDir, и его подкаталогов. Чтобы задать диск и основной каталог, выберите
диск из списка dcbRootDisk (элемент типа TDriveComboBox).
Основной каталог выбирается из списка dlbRootDir (элемент типа TDirectoryListBox).
Оба элемента находятся в групповом поле gbServerProperties. Двойной
щелчок на dcbRootDisk и dlbRootDir автоматически задает значения
свойств RootDisk и
RootDir. Например, значение свойства RootDisk
задается в обработчике OnDblClick элемента dcbRootDisk следующим
образом:
procedure TfrmMain.dcbRootDiskDblClick(Sender: TObject); begin CsKeeper1.RootDisk := dcbRootDisk.Drive; end; Кроме того, новый каталог можно создать, не отходя от вкладки Options, — нажмите кнопку Make Dir, и на экране появится форма frmMkDir для ввода имени создаваемого каталога. Затем двойной щелчок на новом каталоге в списке dlbRootDir задает новое значение свойства RootDir. Группа переключателей rgTransfer используется для выбора стандартного режима пересылки файлов. По умолчанию выбирается режим Stream, то есть файл передается в виде однородного потока байтов. Режимы Block и Compressed необходимы для реализации команды REST, которая позволяет возобновить пересылку файла с того места, где она была прервана. Переключатели Block и Compressed, а следовательно, и команда REST недоступны в текущей версии CsKeeper. Во время выполнения программы переключатели Block и Compressed блокируются. Поэтому KEEPER32 не сможет выполнить команду MODE с параметром BLOCK или COMPRESSED. Вероятно, в будущем я добавлю поддержку этих двух режимов — конечно, при желании вы тоже можете этим заняться. Впрочем, эти режимы используются довольно редко. Протокол FTP позволяет выбрать тип файловой структуры (хотя все значения, кроме File, считаются пережитками прошлого и почти не используются). Тип файловой структуры может принимать три значения — File (то есть однородный файл), Record и Page. По умолчанию CsKeeper устанавливает в группе rgFileStructure переключатель File. Текущая версия CsKeeper не поддерживает работу с файловыми структурами Record и Page и отказывается выполнять полученную от FTP-клиента команду STRU для этих режимов. Чтобы сохранить параметры, введенные на вкладке Options, нажмите кнопку Save в групповом поле gbServerProperties. При этом вызывается процедура SavePropSettings (см. листинг 7.1). Кнопка Cancel отменяет изменения конфигурации (но лишь в том случае, если они еще не были сохранены в реестре). Листинг 7.1. Процедура SavePropSettings procedure TfrmMain.SavePropSettings; var
Reg : TRegistry;
begin
Reg := TRegistry.Create;
try
Reg.OpenKey(FtpServerKey,TRUE);
Reg.WriteString('DRootDisk',dcbRootDisk.Drive);
finally
Reg.CloseKey;
end;
try
Reg.OpenKey(FtpServerKey,TRUE);
Reg.WriteString('DRootDir',
dlbRootDir.Directory);
finally
Reg.CloseKey;
end;
try
Reg.OpenKey(FtpServerKey,TRUE);
case rgTransfer.ItemIndex of
0 :Reg.WriteString('DTransferMode',
FtpTransferStr[STREAM]);
1 :Reg.WriteString('DTransferMode',
FtpTransferStr[BLOCK]);
2 :Reg.WriteString('DTransferMode',
FtpTransferStr[COMPRESSED]);
end;
finally
Reg.CloseKey;
end;
try
Reg.OpenKey(FtpServerKey,TRUE);
case rgFileStructure.ItemIndex of
0 :Reg.WriteString('DFileStructure',
FtpFileStructStr[NOREC]);
1 :Reg.WriteString('DFileStructure',
FtpFileStructStr[REC]);
2 :Reg.WriteString('DFileStructure',
FtpFileStructStr[PAGE]);
end;
finally
Reg.CloseKey;
end;
Reg.Free;
end;
Вопросы безопасностиБезопасность считается одной из самых больших проблем в Internet. В программе KEEPER32 я реализовал лишь самые примитивные меры по обеспечению безопасности доступа. Если вы захотите усовершенствовать KEEPER32, в этой области перед вами открываются великолепные возможности.В групповом поле gbSecurity можно указать, какие действия разрешаются FTP-клиентам, а какие нет. Например, вы можете запретить клиентам удалять каталоги на сервере, для этого следует лишь снять флажок cbDeleteDir. Если вы не хотите, чтобы программа KEEPER32 разрешала клиентам передаватьFRcvBuffer свои файлы на сервер, снимите флажок cbUpload. Внесенные изменения сохраняются кнопкой Save, при нажатии которой вызывается процедура SaveSecure Settings. KEEPER32 можно слегка защитить от злонамеренных хакеров посредством ведения списка IP-адресов тех клиентов, которые уже пытались вызвать хаос в вашей системе. Если IP-адрес подключающегося FTP-клиента присутствует в «черном» списке lbBadIPAddrs, CsKeeper1 разрывает соединение. Для добавления, удаления и сохранения «плохих» IP-адресов используются кнопки Add, Remove и Save соответственно. На рис. 7.4 показана вкладка tsOptions после ввода списка нежелательных IP-адресов.
Рис. 7.4. Список нежелательных IP-адресов, которым KEEPER32 отказывает в установлении соединения Информационные сообщения для клиентов Иногда бывает нужно сообщить подключающимся FTP-клиентам об изменениях в FTP-услугах, предоставляемых KEEPER32, вывести другие информационные сообщения или инструкции («каталог pub/incoming ликвидирован…»). Такие сообщения обычно передаются пользователям при установлении или разрыве соединения. Они называются «приветственными» (welcome) и «прощальными» (farewell) сообщениями соответственно. Вы можете ввести такие сообщения, нажимая кнопку Edit в групповом поле gbMessages. При этом на экране появляется форма frmMessages. На ней содержится элемент pcMessages типа TPageControl, имеющий две вкладки, tsWelcome и tsFarewell. На обеих вкладках присутствуют элементы Memo, в которых редактируется текст сообщений. Кнопка Save сохраняет текущее сообщение в текстовом файле. Внешний вид формы frmMessages показан на рис. 7.5. Указывая имена файлов в свойствах Welcome и Farewell компонента CsKeeper1, вы определяете местонахождение хранящихся сообщений. Когда KEEPER32 принимает подключающегося клиента, компонент CsKeeper1 использует свойство Welcome для поиска и открытия файла с текстом сообщения, отображаемого во время регистра ции.
Рис. 7.5. Форма для ввода приветственных и прощальных сообщений Где и как хранится конфигурацияВсе параметры конфигурации, не считая текстовых файлов с приветственным и прощальным сообщениями, хранятся в системном реестре Windows 95 или NT4.0. Для загрузки и сохранения этих сообщений используется класс Delphi TRegistry. При запуске приложения KEEPER32 обработчик frmMain.OnCreate вызывает процедуру LoadSettings для чтения параметров из реестра Windows. Листинг 7.2 показывает, как это делается. После чтения из реестра LoadSettings обновляет свойства CsKeeper1 в соответствии с полученными значениями.Листинг 7.2. Процедура LoadSettings procedure TfrmMain.LoadSettings;
var
Reg : TRegistry;
Count : Integer;
IPName : String;
begin
Reg := TRegistry.Create;
// Чтение параметров
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DRootDisk') then
CsKeeper1.RootDisk
:= Reg.ReadString('DRootDisk')
else
CsKeeper1.RootDisk := '';
if Reg.ValueExists('DRootDir') then
CsKeeper1.RootDir := Reg.ReadString('DRootDir')
else
CsKeeper1.RootDir := '';
finally
Reg.CloseKey;
end;
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DTransferMode') then
begin
OldTransferMode
:= Reg.ReadString('DTransferMode');
if UpperCase(OldTransferMode) =
UpperCase(FtpTransferStr[STREAM]) then
begin
CsKeeper1.Transfer := STREAM;
rgTransfer.ItemIndex := 0;
end;
if UpperCase(OldTransferMode) =
UpperCase(FtpTransferStr[BLOCK]) then
begin
CsKeeper1.Transfer := BLOCK;
rgTransfer.ItemIndex := 1;
end;
if UpperCase(OldTransferMode) =
UpperCase(FtpTransferStr[COMPRESSED])
then
begin
CsKeeper1.Transfer := COMPRESSED;
rgTransfer.ItemIndex := 2;
end;
end else
begin
OldTransferMode
:= UpperCase(FtpTransferStr[STREAM]);
CsKeeper1.Transfer := STREAM;
end;
finally
Reg.CloseKey;
end;
// Свойство файловой структуры
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DFileStructure') then
begin
OldFileStruct
:= Reg.ReadString('DFileStructure');
if UpperCase(OldFileStruct) =
UpperCase(FtpFileStructStr[NOREC]) then
begin
CsKeeper1.FileStruct := NOREC;
rgFileStructure.ItemIndex := 0;
end;
if UpperCase(OldFileStruct) =
UpperCase(FtpFileStructStr[REC]) then
begin
CsKeeper1.FileStruct := REC;
rgFileStructure.ItemIndex := 1;
end;
if UpperCase(OldFileStruct) =
UpperCase(FtpFileStructStr[PAGE]) then
begin
CsKeeper1.FileStruct := PAGE;
rgFileStructure.ItemIndex := 2;
end;
end else
begin
OldFileStruct
:= UpperCase(FtpFileStructStr[NOREC]);
CsKeeper1.FileStruct := NOREC;
rgFileStructure.ItemIndex := 0;
end;
finally
Reg.CloseKey;
end;
// Разрешение на создание новых каталогов
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DCreateNewDir') then
begin
OldMkDir := Reg.ReadBool('DCreateNewDir');
CsKeeper1.CreateDir := OldMkDir;
if OldMkDir then
cbAllowMkDir.State := cbChecked
else
cbAllowMkDir.State := cbUnChecked;
end else
begin
OldMkDir := FALSE;
CsKeeper1.CreateDir := OldMkDir;
end;
finally
Reg.CloseKey;
end;
// Разрешение на удаление каталогов
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DDeleteDir') then
begin
OldDeleteDir := Reg.ReadBool('DDeleteDir');
CsKeeper1.DeleteDir := OldDeleteDir;
if OldDeleteDir then
cbDeleteDir.State := cbChecked
else
cbDeleteDir.State := cbUnChecked;
end else
begin
OldDeleteDir := FALSE;
CsKeeper1.DeleteDir := OldDeleteDir;
cbDeleteDir.State := cbUnChecked;
end;
finally
Reg.CloseKey;
end;
// Разрешение на передачу файлов
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DUpLoads') then
begin
OldUpLoads := Reg.ReadBool('DUpLoads');
CsKeeper1.UpLoads := OldUpLoads;
if OldUpLoads then
cbUpLoad.State := cbChecked
else
cbUpLoad.State := cbUnChecked;
end else
begin
OldUpLoads := FALSE;
CsKeeper1.UpLoads := OldUpLoads;
cbUpLoad.State := cbUnChecked;
end;
finally
Reg.CloseKey;
end;
try
Reg.OpenKey(FtpServerKey, TRUE);
if Reg.ValueExists('DNoBannedIPs') then
NoOfBannedIPs := Reg.ReadInteger
('DNoBannedIPs')
else
NoOfBannedIPs := 1;
finally
Reg.CloseKey;
end;
// Список запрещенных IP-адресов
for Count := 0 to NoOfBannedIPs - 1 do
begin
IPName := Concat('IPName', IntToStr(Count));
try
Reg.OpenKey(FtpServerKey + '\IPs' + '\
' + IPName, TRUE);
if Reg.ValueExists('IPName') then
lbBadIPAddrs.Items.Add(Reg.ReadString
('IPName'))
else
lbBadIPAddrs.Items.Add('');
OldBannedIPsList.Add(lbBadIPAddrs.Items.Strings
[Count]);
finally
Reg.CloseKey;
end;
end; // цикл for
with CsKeeper1 do
begin
if Length(RootDisk) > 0 then
dcbRootDisk.Drive := Char(RootDisk[1])
else
dcbRootDisk.Drive := 'C';
if Length(RootDir) > 0 then
dlbRootDir.Directory := RootDir;
for Count := 0 to NoOfBannedIPs - 1 do
BadIPs.Add(lbBadIPAddrs.Items.Strings[Count]);
end;
Reg.Free;
end;
Открываемся!После завершения конфигурирования компонента FTP-сервера можно запускать KEEPER32. При нажатии кнопки Start вызывается метод CsKeeper1.Start Server. На рис. 7.6 показан вид приложения, готового к обслуживанию FTP-клиентов.Метод CsKeeper1.StartServer вызывает процедуру GetHome, чтобы изменить текущий диск и основной каталог в соответствии со значениями FRootDisk и FRootDir, загружаемыми процедурой LoadSettings. Вывод списка каталогов и файловПосле запуска сервера вызывается метод GetDirList, который создает текстовый файл INDEX.TXT со списком всех каталогов и файлов, находящихся в основном каталоге. Для построения списка используются функции FindFirst и FindNext (см. листинг 7.3).К сожалению, для представления списка каталогов и файлов не существует стандартного формата. Формат изменяется в зависимости от операционной системы; это одна из проблем, с которыми приходится иметь дело FTP-клиентам. Наш сервер CsKeeper при создании файла INDEX.TXT использует «стандартный» (более или менее) формат Unix. Этот файл пересылается FTP- клиенту после успешной регистрации, а также при каждом удалении, создании или смене каталога.
Рис. 7.6. Программа KEEPER32 готова к обслуживанию клиентов Листинг 7.3. Процедура GetDirList procedure TCsKeeper.GetDirList;
var
F : TextFile;
SearchRec : TSearchRec;
SizeStr, FileName, S : String;
TDate : TDateTime;
Result, K, L : Integer;
begin
AssignFile(F, DirListFile);
Rewrite(F);
if Pos('\',FDirPath) = length(FDirPath) then
FileName := Concat(FDirPath,'*.*')
else
if Pos('\',FDirPath) < length(FDirPath) then
FileName := Concat(FDirPath,'\*.*');
Result := FindFirst(FileName,
faAnyFile, SearchRec);
if Result <> 0 then
begin
Status := Failure;
Exit;
end;
try
TDate := FileDateToDateTime(SearchRec.Time);
except
on EConvertError do
begin
Status := Failure;
Data := '500 Internal error';
closesocket(FSocket);
Exit;
end;
end;
S := FormatDateTime('mmm dd hh'':''mm',TDate);
if DirectoryExists(SearchRec.Name) then
writeln(F,
'drwxrwxrwx 1 noone nogroup ','0','
',S,' ',SearchRec.Name)
else
begin
{ вычисляем длину строки для размера файла }
SizeStr := IntToStr(SearchRec.Size);
L := Length(SizeStr);
for K := 9 - L downto 1 do
SizeStr := ConCat(' ',SizeStr);
write(F,'-rwxrwxrwx 1 noone nogroup');
writeln(F, SizeStr,' ',S,' ',SearchRec.Name);
end;
while Result = 0 do
begin
TDate := FileDateToDateTime(SearchRec.Time);
S := FormatDateTime('mmm dd hh'':''mm',TDate);
if DirectoryExists(SearchRec.Name) then
writeln(F,
'drwxrwxrwx 1 noone nogroup
','0',' ',S,' ',SearchRec.Name)
else
begin
SizeStr := IntToStr(SearchRec.Size);
L := Length(SizeStr);
for K := 9 - L downto 1 do
SizeStr := ConCat(' ',SizeStr);
write(F,'-rwxrwxrwx 1 noone nogroup');
writeln(F, SizeStr,' ',S,' ',SearchRec.Name);
end;
Result := FindNext(SearchRec);
end;
SysUtils.FindClose(SearchRec);
CloseFile(F);
end;
Как и в случае с CsShopper, процедура CsKeeper1.OnInfo передает KEEPER32 сообщения, отображаемые затем в Memo-элементе memStatus (см. рис. 7.7). Любые ошибки FTP передаются обработчиком CsKeeper1.OnError на панель pnErrorMsg.
Рис. 7.7. KEEPER32 с сообщениями о FTP-транзакциях
после
Создание прослушивающего сокетаДо настоящего момента мы занимались подготовкой, причем вся работа в основном сводилась к созданию текстовых файлов. Теперь настало время воспользоваться Windows Sockets. Прежде всего необходимо вызвать CsSocket.Get Server, чтобы инициализировать структуры данных, необходимые для сервиса FTP. Процедура инициализации приведена в листинге 7.4.Листинг 7.4. Метод CsSocket.GetServer procedure TCsSocket.GetServer; begin GetServ; if Status = Failure then Exit; FSockAddress.sin_family := PF_INET; FSockAddress.sin_port := FServ^.s_port; FSockAddress.sin_addr.s_addr := htonl(INADDR_ANY); FRemoteName := LocalName; FSocket := CreateSocket; end; После того как все необходимые структуры
данных инициализированы, GetServer вызывает CreateSocket,
чтобы создать прослушивающий сокет FSocket. Далее мы вызываем функцию
Winsock API с именем WSAAsyncSelect, чтобы приказать Winsock DLL
извещать CsKeeper о событиях сокета посредством отправки сообщений в адрес
Wnd (это логический номер окна типа HWND). Для этого используется
следующая строка:
if WSAAsyncSelect(FSocket, Wnd, FTP_EVENT, FD_ACCEPT) = SOCKET_ERROR then Затем мы вызываем bind, еще одну функцию Winsock API, чтобы связать локальное имя с безымянным сокетом FSocket, а также с адресом хоста и номером порта. Это необходимо для прослушивания порта на предмет устанав ливаемых соединений. Функция listen сообщает CsKeeper о необходимости прослушивания порта 21. После вызова этой функции программа KEEPER32 готова к установке соединения через этот порт. Как вас обслуживают?Когда FTP-клиент соединяется с TCP-портом 21, Winsock DLL посылает сообщение FTP_EVENT. В результате процедура FtpEvent активизируется и начинает ожидать от сокета информационное сообщение FD_ACCEPT. В ветви FD_ACCEPT оператора case процедура FtpEvent создает сокет FClientSocket с помощью функции accept:FClientSocket := accept (FSocketNo, @ClientSockAddr), @FAddrSize); Затем мы вызываем функцию Winsock API с именем getpeername, чтобы узнать IP-адрес клиента. Получив IP-адрес, CsKeeper поочередно сравнивает его со всеми строками адресов «плохих» клиентов, хранящимися в списке CsKeeper.FBadIPs. Если будет найдено совпадение, CsKeeper посылает предупреждающее сообщение, отсоединяет нежелательного FTP-клиента и возвращается в состояние прослушивания. Если же клиент признан добропорядоч ным, CsKeeper вызывает LoginUser для выполнения оставшейся части регистрации. Вход строго по одномуЧтобы предотвратить попытки соединения со стороны новых FTP-клиентов, LoginUser вызывает функцию WSAAsyncSelect с последним параметром, равным 0 — при этом Winsock DLL перестает оповещать прослушивающий сокет FSocket. Это происходит в следующей строке:if WSAAsyncSelect(FSocket, Wnd, FTP_EVENT,
0) = SOCKET_ERROR then
В результате все остальные FTP-клиенты будут получать отказ в обслуживании до тех пор, пока CsKeeper не закончит работу с текущим клиентом. Затем следует очередной вызов WSAAsyncSelect:
if WSAAsyncSelect(FClientSocket, Wnd, FTP_EVENT, FD_READ OR FD_CLOSE OR FD_OOB OR FD_WRITE) = SOCKET_ERROR then begin { продолжение... } Этот вызов обеспечивает уведомление со стороны Winsock о любых событиях сокета FClientSocket. После завершения регистрации CsKeeper1 ожидает поступления по управляющему соединению других FTP-команд. Когда FTP-клиент выдает команду (например, RETR), FtpEvent получает ее, перехватывая событие FD_READ, сгенерированное Winsock DLL. В ветви FD_READ оператора case вызывается процедура DecodeFTPCmd, которая обрабатывает команды, посылаемые FTP-клиентом. DecodeFTPCmd декодирует команду и вызывает соответствующую процедуру. Если команда не опознана, CsKeeper1 посылает FTP-клиенту код ошибки. Процесс обработки FTP-команд в процедуре DecodeFTPCmd показан в листинге 7.5. Именно здесь находится «сердце» компонента CsKeeper. Листинг 7.5. Метод DecodeFTPCmd procedure TCsKeeper.DecodeFTPCmd
(SockNo : TSocket;
CmdStr : CharArray; S : String);
var
FtpCmd, Selector : TFtpCmds;
DirStr, FileName,
Line, Port1Str, Port2Str, S1, TempStr : String;
Finished : Boolean;
Count : Byte;
begin
FtpCmd := UNK;
Finished := FALSE;
Count := 1;
S1 := '';
TempStr := StrPas(CmdStr);
while not Finished do
begin
if (TempStr[Count] = ' ') or ((TempStr[Count]
= #13) and
(TempStr[Count + 1] = #10)) then
begin
Finished := TRUE;
end else
begin
S1 := ConCat(S1,TempStr[Count]);
Inc(Count);
end;
end;
Selector := PWD;
Status := Failure; { На всякий случай
предположим, что произошла неудача }
Finished := FALSE;
if S1 = '' then Exit; { Пустые строки не
обрабатываются }
while not Finished do
begin
if CompareText(S1, FtpCmdStr[Selector])
= 0 then
begin
FtpCmd := Selector;
Status := Success;
break;
end else
begin
if Selector = UNK then
begin
Status := Failure;
Finished := TRUE;
end;
if not Finished then Inc(Selector);
end;
end;
if Status = Failure then
begin
Info := Concat('Unrecognised command received
from ', FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 Unrecognised
command');
Status := Failure;
Exit;
end;
case FtpCmd of
PWD : begin
Info := Concat('PWD command received from ',
FClientAddrStr);
InfoEvent(Info);
GetDir(0, DirStr);
SendFtpCode(FClientSocket,'257 Working
directory is '+ DirStr);
end;
RETR : begin
Info := Concat('RETR command
received from ',
FClientAddrStr);
InfoEvent(Info);
FileName := Copy(TempStr,
Pos(' ', TempStr)+1,
Length(TempStr));
if Pos(#13, FileName) > 0 then
FileName := Copy(FileName, 1, Pos(#13,
FileName)-1);
Info := Concat('Sending file ',FileName,
' to ', FClientAddrStr);
InfoEvent(Info);
if FFileType = IMAGE then
begin
Info := Concat('Using IMAGE type');
InfoEvent(Info);
SendFtpCode(FClientSocket,
'150 Opening BINARY data connection
for ' + FileName)
end
else
begin
Info := Concat('Using ASCII type');
InfoEvent(Info);
SendFtpCode(FClientSocket,
'150 Opening ASCII data
connection for ' + FileName);
end;
SendFile(FileName);
end;
STOR : begin
Info := Concat('STOR command
received from ', FClientAddrStr);
InfoEvent(Info);
if FUpLoads then
begin
FileName := Copy(TempStr,
Pos(' ', TempStr)+1,
Length(TempStr));
if Pos(#13, FileName) > 0
then
FileName := Copy(FileName, 1,
Pos(#13, FileName)-1);
Info := Concat('Sending file ',
FileName, ' to ',
FClientAddrStr);
InfoEvent(Info);
if FFileType = IMAGE then
begin
Info := Concat('Using IMAGE type');
InfoEvent(Info);
SendFtpCode(FClientSocket,
'150 Opening BINARY data
connection for ' + FileName)
end
else
begin
Info := Concat('Using ASCII type');
InfoEvent(Info);
SendFtpCode(FClientSocket,
'150 Opening ASCII data
connection for ' + FileName);
end;
GetFile(FileName);
end else
SendFtpCode(FClientSocket,
'500 STOR command not executed
(not allowed)');
end;
USER : begin
{ Декодируем строку }
if Pos('ANONYMOUS',UpperCase(TempStr))
> 0 then
begin
Info := Concat('USER command received
from ',
FClientAddrStr);
InfoEvent(Info);
Info := Concat('Anonymous login
received from ', FClientAddrStr);
InfoEvent(Info);
FUserType := ANONYMOUS;
SendFtpCode(FClientSocket,
'331- Anonymous user accepted.');
SendFtpCode(FClientSocket,
'331 Send in your password,
please');
Info := Concat(FClientAddrStr,'
logged in as anonymous');
InfoEvent(Info);
end else
begin
FUserType := ACCOUNT;
SendFtpCode(FClientSocket,'500 '
+ FtpCmdStr[ACCT] +
' command not implemented');
end;
end;
QUIT : begin
Info := Concat('QUIT command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'221
Goodbye from Keeper!');
Info := FClientAddrStr;
Info := ConCat(Info, ' logged out');
InfoEvent(Info);
closesocket(FClientSocket);
FClientSocket := INVALID_SOCKET;
if FNoOfUsers >= 1 then
Dec(FNoOfUsers);
{ Переходим к основному устройству
и каталогу }
GetHome;
GetDirList;
{ Возвращаемся в состояние
прослушивания }
if WSAAsyncSelect(FSocket, Wnd,
FTP_EVENT, FD_ACCEPT)
= SOCKET_ERROR then
begin
Info := Concat('ERROR : 11
[',FClientAddrStr,'] ',
WSAErrorMsg);
InfoEvent(Info);
Status := Failure;
Exit;
end;
end;
PASS : begin
{ Тип пользователя - ? }
if FUserType = ANONYMOUS then
begin
Info := Concat('PASS command received
from ',
FClientAddrStr);
InfoEvent(Info);
{ Получаем адрес электронной почты
пользователя }
SendFtpCode(FClientSocket,
'230 User logged in.
Go ahead!');
end;
end;
CDUP : begin
Info := Concat('CDUP command
received from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[CDUP] +
' command not implemented');
end;
CWD : begin
Info := Concat('CWD command received
from ',
FClientAddrStr);
InfoEvent(Info);
{$I-}
{ Переходим в каталог, указанный в
Edit1 }
FileName := Copy(TempStr, Pos(' ',
TempStr)+1,
Length(TempStr));
if Pos(#13, FileName) > 0 then
FileName := Copy(FileName, 1, Pos(#13,
FileName)-1);
If DirectoryExists(FileName) then
ChDir(FileName)
else
begin
Status := Failure;
SendFtpCode(FClientSocket,'500 Not
a directory');
Exit;
end;
if IOResult <> 0 then
SendFtpCode(FClientSocket,'500 Cannot
find directory')
else
begin
SendFtpCode(FClientSocket,'200 Changed
directory');
GetDir(0,FDirPath);
GetDirList;
end;
end;
LIST : begin
Info := Concat('LIST command received
from ',
FClientAddrStr);
InfoEvent(Info);
GetDirList;
Info := Concat('Sending LIST to ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'150
Opening Ascii connection');
SendFile(DirListFile);
end;
PORT : begin
Info := Concat('PORT command received
from ',
FClientAddrStr);
InfoEvent(Info);
Count := Length(TempStr);
Port1Str := '';
Port2Str := '';
if (TempStr[Count] = #10) and
(TempStr[Count-1] = #13) then
Dec(Count,2); { не включать CR/LF!}
while TempStr[Count] <> ',' do
begin
Port2Str := Concat(TempStr[Count],
Port2Str);
Dec(Count);
end;
Dec(Count);
while TempStr[Count] <> ',' do
begin
Port1Str := Concat(TempStr[Count],
Port1Str);
Dec(Count);
end;
FPort2 := StrToInt(Port2Str);
FPort1 := StrToInt(Port1Str);
FPortNo := FPort2 + 1024;
Info := Concat('Port No received ',
IntToStr(FPortNo),
' from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'200 PORT
command okay');
FClientSockAddr.sin_port := FPortNo;
{ Открываем соединение данных }
end;
SYST : begin
Info := Concat('SYST command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'215
Unix Keeper 1.0');
end;
HELP : begin
Info := Concat('HELP command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,
'211- HELP Commands implemented
at this site:');
SendFtpCode(FClientSocket,
'211- QUIT RETR USER PASS LIST PORT
CWD TYPE PWD');
SendFtpCode(FClientSocket,'211 ');
end;
FTYPE: begin
if Pos('A', UpperCase(TempStr)) > 0
then
begin
FFileType := ASCII;
SendFtpCode(FClientSocket,'200
TYPE ASCII');
end
else
if Pos('I', UpperCase(TempStr))
> 0 then
begin
FFileType := IMAGE;
SendFtpCode(FClientSocket,'200
TYPE BINARY');
end;
end;
MODE : begin
Info := Concat('MODE command received
from ',
FClientAddrStr);
InfoEvent(Info);
if Pos(' S', Uppercase(TempStr)) >
0 then
FTransfer := STREAM
else
if Pos(' B', Uppercase(TempStr)) >
0 then
FTransfer := BLOCK
else
FTransfer := COMPRESSED;
end;
NLST : begin
Info := Concat('NLST command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 '
+ FtpCmdStr[NLST] +
' command not
implemented');
end;
QUOTE : begin
Info := Concat('QUOTE command
received from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[QUOTE] +
' command not implemented');
end;
PASV : begin
Info := Concat('PASV command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[PASV] +
' command not implemented');
end;
SITE : begin
Info := Concat('SITE command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[SITE] +
' command not
implemented');
end;
MKD : begin
if FCreateDir then
begin
Info := Concat('MKDIR command
received from ',
FClientAddrStr);
InfoEvent(Info);
Delete(TempStr,1,Pos(' ',TempStr));
Delete(TempStr,Pos(#13,TempStr),
Length(TempStr));
{$I-}
MkDir(TempStr);
if IOResult <> 0 then
begin
Info := Concat('MKDIR command
failed to create ',
TempStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 '
+ FtpCmdStr[MKD] +
' command not
implemented');
end
else
begin
Info := Concat('MKDIR command to
create ',TempStr,
' executed
successfully');
InfoEvent(Info);
SendFtpCode(FClientSocket,'200 ' +
FtpCmdStr[MKD] +
' command received OK');
end;
end else
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[MKD] +
' command not implemented');
end;
RMD : begin
Info := Concat('RMD command received
from ',
FClientAddrStr);
InfoEvent(Info);
if FDeleteDir then
begin
delete(TempStr,1, Pos(' ',TempStr));
delete(TempStr, Pos(#13,TempStr),
Length(TempStr));
{$I-}
RmDir(TempStr);
if IOResult <> 0 then
begin
Info := Concat('RMD command failed to
delete ',TempStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[RMD] +
' command failed');
end
else
begin
Info := Concat('RMD command to
delete ',TempStr,
' executed
successfully');
InfoEvent(Info);
SendFtpCode(FClientSocket,'200 ' +
FtpCmdStr[RMD] +
' command received OK');
end;
end else
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[RMD] +
' command not executed');
end;
STRU : begin
Info := Concat('STRU command
received from ',
FClientAddrStr);
InfoEvent(Info);
if Pos(' F', Uppercase(TempStr)) >
0 then
FFileStruct := NOREC
else
if Pos(' R', Uppercase(TempStr)) >
0 then
FFileStruct := REC
else
FFileStruct := PAGE;
end;
STAT : begin
Info := Concat('STAT command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[STAT] +
' command not
implemented');
end;
ACCT : begin
Info := Concat('ACCT command
received from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'500 ' +
FtpCmdStr[ACCT] +
' command not implemented');
end;
NOOP : begin
Info := Concat('NOOP command received
from ',
FClientAddrStr);
InfoEvent(Info);
SendFtpCode(FClientSocket,'200 ' +
FtpCmdStr[NOOP] +
' command received OK');
end;
end;
end;
При получении от FTP-клиента команды LIST CsKeeper вызывает SendFile, чтобы передать файл INDEX.TXT через соединение данных. После того как пересылка будет завершена, CsKeeper закрывает соединение данных. Соединение данных всегда является временным, в отличие от постоянного управляющего соединения. Мне, пожалуйста, вот это…Разумеется, raison d'кtre всего протокола FTP — пересылка файлов, поэтому нет ничего удивительного в том, что из полного набора FTP-команд чаще всего используются команды выборки и сохранения RETR и STOR. Команда RETR предназначена для получения файла с сервера, а STOR — для принятия и сохранения сервером файла, передаваемого клиентом.При получении команды RETR процедура DecodeFTPCmd анализирует переданную командную строку, и с помощью кода, расположенного в ветви RETR большого оператора case, извлекает из нее имя передаваемого файла. Полученное имя передается процедуре SendFile, которая и выполняет пересылку. Чтобы обеспечить прием файла FTP-клиентом, CsKeeper вызывает SendFTPCode с кодом 150, сообщая тем самым клиенту о необходимости прослушивания данных на ранее заданном порте. В самой пересылке файла нет ничего сверхъестественного. SendFile создает локальный сокет с именем LocalSocket и затем вызывает функцию connect, чтобы открыть соединение данных. После установки соединения CsKeeper открывает файл, из которого должны читаться передаваемые данные. Процедура BlockRead в цикле repeat…until читает данные блок за блоком, а функция send передает их. Когда данных для пересылки не остается, CsKeeper закрывает файл и уничтожает соединение данных, вызывая closesocket для закрытия сокета LocalSocket. Затем CsKeeper вызывает SendFTPCode, чтобы передать FTP-клиенту код ответа 226, сообщающий о том, что передача файла завершена. Сохраните, пожалуйста…STOR — зеркальное отражение команды RETR. Вместо того чтобы передавать файл клиенту, CsKeeper сохраняет (stores) полученный файл, отсюда и название команды. При получении компонентом CsKeeper команды STOR процедура DecodeFTPCmd анализирует командную строку и переходит к ветви STOR оператора case, в котором обрабатываются различные команды. Если значение FUpLoads равно TRUE (помните, мы можем запретить передачу файлов на сервер, снимая соответствующий флажок на вкладке Options), вызывается метод TCsKeeper.GetFile. В противном случае DecodeFTPCmd посылает отрицательный ответ с кодом 500.TCsKeeper.GetFile создает для соединения
данных локальный сокет с именем LocalSocket; для этого используется
вызов функции connect, входящей в Winsock API:
if connect (LocalSocket, DataS, SizeOf(TSockAddrIn))=
SOCKET_ERROR then
После открытия файла мы сохраняем поступающие
данные в цикле while…do с помощью функций recv (Winsock API)
и BlockWrite:
while not Finished do
После того как все данные от клиента будут приняты, TCsKeeper.GetFile закрывает установленное через LocalSocket соединение данных и передает клиенту положительный код ответа 226 с помощью процедуры SendFtpCode. Закрыто на переучетТеперь у вас появился собственный, вполне работоспособный FTP-сервер, и создать его было не так уж сложно. Более того, как показывает мой собственный опыт, написать компонент для FTP-сервера значительно проще, чем для FTP-клиента, особенно если выбросить из рабочего словаря сервера некоторые хитроумные и редко используемые FTP-команды.Тем не менее существует одно усовершенствование,
которое сделает CsKeeper намного более полезным — речь идет о параллельной
обработке. Она позволяет одновременно подключать к серверу и обслуживать
сразу несколько FTP-клиентов. Практически все современные серверы поддерживают
параллельную обработку, особенно если учесть, что на рынке серверов сейчас
господствуют операционные системы Windows NT и Unix. Чтобы реализовать
параллельную обработку в FTP-сервере, нам пришлось бы изучать реализацию
многопоточности (multithreading) в Delphi. Это весьма достойная тема, но
она, к сожалению, выходит за рамки этой главы.
|
|
|