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

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