Глава 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

ABOR

ACCT*

ALLO*

APPE*

CDUP

CWD

DELE

HELP

LIST

MKD

MODE

NLIST*

NOOP

PASS

PASV*

PORT

PWD

QUIT

REIN*

RMD

RNFR*

RNTO*

REST*

RETR

SITE

SMNT*

STAT*

STOR

STOU*

STRU*

SYST

TYPE

USER*

Прерывание текущей пересылки файла

Передача информации о ресурсах пользователя

Выделение места под новый файл

Добавление данных в существующий файл

Переход в родительский каталог

Переход в другой каталог

Удаление файла, выбранного пользователем

Запрос справочной информации о FTP-команде

Запрос списка файлов текущего каталога

Создание нового каталога

Использование режима пересылки, выбранного клиентом

Запрос потока с именами файлов

Передача сервером ответа «OK»

Передача пароля во время регистрации

Прослушивание сервером конкретного порта данных

Использование сервером порта данных, выбранного клиентом

Запрос имени текущего каталога

Завершение FTP-сеанса

Повторная инициализация сеанса

Удаление каталога

Передача имени файла, который следует переименовать

Передача нового имени файла. Команда должна передаваться после RNFR

Возобновление прерванной пересылки файла

Получение файла с сервера

Получение информации о специфических услугах сервера

Монтирование другой файловой системы на сервере

Запрос информации о статусе

Запрос на сохранение файла

Сохранение файла с уникальным именем на сервере

Запрос на использование файловой структуры, выбранной клиентом

Запрос типа операционной системы

Выбор типа пересылаемого файла

Передача имени пользователя во время регистрации команда не реализована в текущей версии CsKeeper

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-транзакциях после
выполнения команды LIST

Создание прослушивающего сокета

До настоящего момента мы занимались подготовкой, причем вся работа в основном сводилась к созданию текстовых файлов. Теперь настало время воспользоваться 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
begin
Response := recv(LocalSocket, Buffer, SizeOf(Buffer), 0);
{ пропуск... }
if Response > 0 then
BlockWrite(F, Buffer, Response);
end;

После того как все данные от клиента будут приняты, TCsKeeper.GetFile закрывает установленное через LocalSocket соединение данных и передает клиенту положительный код ответа 226 с помощью процедуры SendFtpCode.

Закрыто на переучет

Теперь у вас появился собственный, вполне работоспособный FTP-сервер, и создать его было не так уж сложно. Более того, как показывает мой собственный опыт, написать компонент для FTP-сервера значительно проще, чем для FTP-клиента, особенно если выбросить из рабочего словаря сервера некоторые хитроумные и редко используемые FTP-команды.

Тем не менее существует одно усовершенствование, которое сделает CsKeeper намного более полезным — речь идет о параллельной обработке. Она позволяет одновременно подключать к серверу и обслуживать сразу несколько FTP-клиентов. Практически все современные серверы поддерживают параллельную обработку, особенно если учесть, что на рынке серверов сейчас господствуют операционные системы Windows NT и Unix. Чтобы реализовать параллельную обработку в FTP-сервере, нам пришлось бы изучать реализацию многопоточности (multithreading) в Delphi. Это весьма достойная тема, но она, к сожалению, выходит за рамки этой главы.

 

Используются технологии uCoz