STM Урок 95. LAN. W5500. FTP Server. Часть 2



В предыдущей части занятия мы настроили проект, немного усовершенствовали код в плане работы с сервером HTTP, также создали самые первые функции для работы с протоколом FTP.

Вернёмся в проект в нашу функцию ftp_receive в файле ftpd.c в то же условие, но только в его противное тело (else). Это тело будет выполняться при условии получения пакета в том случае, если у нас уже создано управляющее соединение. Добавим в данное тело ещё одно условие, которое проверит, что пакет пришёл с данными (не пустой). Зачем нам отвечать на пустые пакеты

  ftpprop.connect_stat = FTP_CONNECT;

}

else

{

  //проверим, что пакет не пустой

  if(len > 0)

  {

  }

}

Теперь перейдём в файл w5500.c и добавим там ещё одну функцию установки указателя в буфере чтения после функции GetReadPointer

//-----------------------------------------------

void SetReadPointer(uint8_t sock_num, uint16_t point)

{

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_RX_RD0, point>>8);

  w5500_writeReg(opcode, Sn_RX_RD1, (uint8_t)point);

}

//-----------------------------------------------

Добавим на данную функцию прототип в заголочном файле.

Вернёмся в файл ftpd.c и заполним тело только что созданного нами условия в функции ftp_receive

if(len > 0)

{

  //Отобразим размер принятых данных

  sprintf(str1,"S%d len buf:0x%04Xrn",sn,len);

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  //примем данные

  point = GetReadPointer(sn);

  //Отобразим адрес данных

  sprintf(str1,"S%d point RX:0x%04Xrn",sn,point);

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  w5500_readSockBuf(sn, point, sect, len);

  //Передвинем указатель

  SetReadPointer(sn, point+len);

  RecvSocket(sn);

  //завершим нулём

  sect[len] = 0;

  //отобразим их в терминальной программе

  sprintf(str1,"S%d: %srn", sn, (char*)sect);

  HAL_UART_Transmit(&huart2,(uint8_t*)(char*)sect,strlen((char*)sect),0x1000);

}

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

Соберём код, прошьём контроллер и опять осуществим попытку соединения с сервером FTP, поглядывая на информацию в терминальной программе

Image06

Все команды принимаются и номально отображаются в терминальной программе.

Теперь нам необходимо на эти команды как-то реагировать.

Создадим выше функцию разбора строки данных пакета, создав сразу ряд переменных, указателей и массивов

//-----------------------------------------------

void ftp_cmd_parse(uint8_t sn, char* buf)

{

  char **cmd_point, *ch, *arg, *ch_tmp;

  char buf_send[200];

  uint16_t len;

  uint16_t offset;

  data_sect_ptr *datasect = (void*)buf_send;

}

//-----------------------------------------------

Вызовем данную функцию в теле условия в конце только что нами добавленного кода

      HAL_UART_Transmit(&huart2,(uint8_t*)(char*)sect,strlen((char*)sect),0x1000);

      ftp_cmd_parse(sn, (char*)sect);

    }

  }

}

else if(sn == FTP_SOCKET_DATA)

Создадим глобальный массив из команд, представленных в строчном виде и преобразованных к нижнему регистру, так как у FTP на этот счёт нет ограничений и команды могут прийти в разных регистрах. По количеству строчных элементов массив должен сходиться с перечислением в заголовочном файле

extern volatile uint16_t tcp_size_wnd;

//-----------------------------------------------

/* Command table */

static char *ftp_commands[] = {

"user",

"acct",

"pass",

"type",

"list",

"cwd",

"dele",

"name",

"quit",

"retr",

"stor",

"port",

"nlst",

"pwd",

"xpwd",

"mkd",

"xmkd",

"xrmd",

"rmd ",

"stru",

"mode",

"syst",

"xmd5",

"xcwd",

"feat",

"pasv",

"size",

"mlsd",

"appe",

NULL

};

//-----------------------------------------------

Вернёмся теперь в функцию ftp_cmd_parse и начнём сочинять её тело, а оно обещает быть немалым.

 

 

Преобразуем пришедшую команду также к нижнему регистру, преобразовывая символы до первого пробела или до окончания данных

data_sect_ptr *datasect = (void*)buf_send;

//преобразуем к нижнему регистру

for (ch = buf; *ch != ' ' && *ch != ''; ch++) *ch = tolower(*ch);

Далее поищем команду в нашем списке

for (ch = buf; *ch != ' ' && *ch != ''; ch++) *ch = tolower(*ch);

// найдём команду в списке

for (cmd_point = ftp_commands; *cmd_point != NULL; cmd_point++)

{

  if (strncmp(*cmd_point, buf, strlen(*cmd_point)) == 0) break;

}

Если не найдём, то отправим в ответ, что команда несуществующая и уйдём из функции

  if (strncmp(*cmd_point, buf, strlen(*cmd_point)) == 0) break;

}

if (*cmd_point == NULL)

{

  sprintf((char*)datasect->data, "500 Unknown command '%s'rn", buf);

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, strlen((char*)datasect->data));

  return;

}

Далее добавим условие, узнающее, авторизован ли клиент на сервере или ещё нет

  return;

}

if (ftpprop.login_stat == FTPS_NOT_LOGIN)

{

}

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

if (ftpprop.login_stat == FTPS_NOT_LOGIN)

{

  switch(cmd_point - ftp_commands)

  {

    case USER_CMD:

    case PASS_CMD:

    case QUIT_CMD:

      break;

    default:

      len = sprintf((char*)datasect->data, "530 Please log in with USER and PASSrn");

      tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

      return;

  }

}

Далее выйдем из условия неавторизованности и установим указатель на аргумент команды

    return;

  }

}

//установим указатель на аргумент команды клиента

arg = &buf[strlen(*cmd_point)];

while(*arg == ' ') arg++;

Дальше начинаем исследовать, какая команда к нам пришла.

Добавим для этого switch

while(*arg == ' ') arg++;

switch (cmd_point - ftp_commands)

{

}

И теперь начнём добавлять в него различные варианты команд.

В данный момент по анализатору трафика мы увидели, что клиетн нам послал на обработку команду USER с аргументом имени пользователя. Вот её мы и обработаем, заодно добавим ветку default

switch (cmd_point - ftp_commands)

{

  case USER_CMD :

    break;

  default: // Invalid

    break;

}

Начнём с default, чтобы потом не забыть.

default: // Invalid

  len =sprintf((char*)datasect->data, "500 Unknown command '%s'rn", arg);

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  HAL_Delay(1000);

  DisconnectSocket(sn); //Разъединяемся

  SocketClosedWait(sn);

  sprintf(str1,"S%d closedrn",sn);

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  OpenSocket(sn,Mode_TCP);

  //Ждём инициализации сокета (статус SOCK_INIT)

  SocketInitWait(sn);

  //Продолжаем слушать сокет

  ListenSocket(sn);

  SocketListenWait(sn);

break;

Здесь мы себя ведём стандартным образом: разъединяем сокет, заново открываем его и начинаем слушать, выведя при этом некоторую информацию об этом в терминальную программу. Задержку я включил для избежания очень быстрого зацикливания, думаю сильно не повредит.

Теперь собственно обработка команды USER.

case USER_CMD :

  sprintf(str1, "USER_CMD : %s", arg);

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  //отправим ответ на команду

  len = strlen(arg);

  arg[len - 1] = 0x00;

  arg[len - 2] = 0x00;

  strcpy(ftpprop.username, arg);

  len = sprintf((char*)datasect->data, "331 Enter PASS commandrn");

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  break;

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

Перейдём теперь в файл w5500.c и подключим там переменную нашей структуры со свойствами

extern http_sock_prop_ptr httpsockprop[8];

extern ftp_prop_ptr ftpprop;

Также чуть не забыл. Надо в функции приёма и обработки пакетов w5500_packetReceive обработать состояние отключенного сокета. Это потребуется в том случае, если сокет отключится по каким-то неведомым причинам, ну например, по инициативе клиента. Когда мы отключаем его преднамеренно, то мы потом с ним опять создаём соединение. А бывают и другие моменты.

Поэтому создадим противный случай условия открытого сокета в данной функции и напишем там соответствующий код

      http_receive(sn);

    }

  }

  else

  {

    //управляющий сокет FTP

    if(sn==FTP_SOCKET_CTRL)

    {

      if(ftpprop.connect_stat) ftpprop.connect_stat=FTP_DISCONNECT;

      if(ftpprop.login_stat==FTPS_LOGIN) ftpprop.login_stat=FTPS_NOT_LOGIN;

      SetReadPointer(FTP_SOCKET_CTRL, 0);

    }

  }

}

То есть, в таком случае мы заново инициализируем настройки и устанавливаем указатель буфера в 0.

 

 

Давайте теперь соберём код и прошьём контроллер и проследим ответ на команду USER со стороны сервера, и также посмотрим, что нам ответит клиент.

Вот результат в терминальной программе

Image07

Мы видим, что клиент нам ответил передачей пароля.

Также посмотрим, что у нас творится в анализаторе трафика

Image08

Здесь мы тажке видим, что наше сообщение клиент получил и ответил нам своим паролем, на что сервер ответил, что это для него неизвестная команда. Само собой, она серверу неизвестна, пока мы её ещё не обработали.

Этим мы и займёмся. Вернёмся в файл ftpd.c и над функцией разбора команд добавим функцию авторизации клиента

//-----------------------------------------------

void ftp_login(uint8_t sn, char * pass)

{

  char buf_send[100];

  uint16_t len;

  data_sect_ptr *datasect = (void*)buf_send;

  len = sprintf((char*)datasect->data, "230 Logged onrn");

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  ftpprop.login_stat = FTPS_LOGIN;

}

//-----------------------------------------------

В данной функции мы устанавливаем указательна данные для передачи, передаём сообщение об удачной авторизации клиента, а также устанавливаем поле структуры со свойством авторизации в авторизованное. Как таковой авторизации у нас не будет собственно, мы не проверяем валидность учётной пары, просто соглашаемся со всем тем, что нам пришлёт клиент.

Теперь вернёмся в наш switch в функцию ftp_cmd_parse и обработаем там команду PASS

  break;

case PASS_CMD :

  len = strlen(arg);

  arg[len - 1] = 0x00;

  arg[len - 2] = 0x00;

  ftp_login(sn, arg);

  break;

default: // Invalid

Тут всё просто. Мы также обнуляем перевод строки и возврат каретки и вызываем функцию авторизации клиента.

Соберём код, прошьём контроллер и посмотрим, что на этот раз от нас потребует клиент.

Результат в терминальной программе

Image09

Клиент отправил нам команду SYST, которая является запросом идентификации операционной системы, на что мы должны ему немного рассказать о нашем сервере.

Также посмотрим анализатор трафика, в котором мы, конечно же, увидим отправку клиенту того, что данная команда для нас неизвестна (пока)

Image10

Причём клиент оказался очень вежливым клиентом и на наш позорный ответ о незнании команды не обиделся и запросил уже следующую команду. Но, я думаю, возможны такие случаи, когда попадётся обичивый клиент, который просто разорвёт с нами соединение, поэтому давайте всё-таки что-нибудь ему ответим

  break;

case SYST_CMD :

  len = sprintf((char*)datasect->data, "215 UNIX emulated by W5500rnrn");

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  break;

default: // Invalid

Проверим реакцию клиента, для чего соберём код и прошьём контроллер.

Реакция та же самая — клиент послал команду FEAT, которая служит для согласования функций, то есть клиент просит рассказать теперь о сервере поподробнее, что он может.

Ответим ему нашими возможностями

  break;

case FEAT_CMD :

  len = sprintf((char*)datasect->data, "211-Features:rn MDTMrn REST STREAMrn SIZErn MLST size*;type*;create*;modify*;rn MLSDrn UTF8rn CLNTrn MFMTrn211 ENDrn");

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  break;

default: // Invalid

Вот такие вот у нас возможности. Также есть требование, что после перечисления всех возможностей нужно отправлять ещё слово END, чтобы клиент понял, что перед ним было последнее свойство сервера. Что это за возможности, я здесь перечислять не буду, так как это можно узнать в специальных источниках по протоколу FTP.

Соберём проект, прошьём контроллер и проверим результат сразу в анализаторе трафика Wireshark, так как там всё-таки информации больше, в терминальной программе мы не видим отчёт о пришедших от нас пакетах

Image11

Мы видим, что ответ от нас пришёл. Посмотрим, как его расшифровал Wireshark

Image12

Вот так вот всё красиво увидел анализатор трафика.

Следующую команду нам послылает клиент CWD, видит ответ сервера о неизвестности и посылает потом едё и PWD.

Первая команда CWD — это просьба клиента о смене текудещего каталога. Имя текущего каталога клиент передаёт в аргументе. Клиент просит перейти в корневой каталог, поэтому в качестве аргумента он передаёт слеш (косую черту).

Потихоньку начнём ему отвечать

  break;

case CWD_CMD:

  len = strlen(arg);

  arg[len - 1] = 0x00;

  arg[len - 2] = 0x00;

  //если не подкаталог

  if(arg[0]=='/')

  {

    strcpy(ftpprop.work_dir, arg);

  }

  break;

default: // Invalid

Здесь мы заносим в поле с именем текущего каталога (папки, директории) переданный аргумент. Только это в том случае, если клиент передал в качестве аргумента именно корневой каталог.

Далее обработаем передачу клиентом в качестве аргумента переход на уровень выше. Это такой случай, когда клиент просит из какого-либо каталога, являющегося на данный момент выйти и перейти на верхний уровень. В этом случае клиент нам передаёт в аргументе две точки

  strcpy(ftpprop.work_dir, arg);

}

//переход на верхний уровень

else if(strncmp(arg,"..", 2) == 0)

{

}

break;

Теперь начнём писать тело данного условия перехода на уровень выше

else if(strncmp(arg,"..", 2) == 0)

{

  //отрежем каталог самого нижнего уровня

  //Найдём последний символ '/'

  offset = 0;

  while(1)

  {

    ch_tmp = strchr(ftpprop.work_dir+offset,'/');

    if(!ch_tmp)

    {

      ftpprop.work_dir[offset] = '';

      break;

    }

    ch = ch_tmp;

    offset = ch_tmp - ftpprop.work_dir + 1;

  }

Здесь мы используем имя текущего каталога хранящегося у нас в определённом поле структуры со свойствами сервера. мы от него будем пытаться отрезать каталог самого нижнего уровня, чтобы текущем каталогом стал каталог более верхнего уровня. Для этого мы ищем последний символ слеша и соответственно в это место вставляем 0, что и будет соответствовать окончанию строки. Затем уберём слеш, если и заканчивается имя каталога, он нам не нужен. Корневого каталога это не касается

    offset = ch_tmp - ftpprop.work_dir + 1;

  }

  //отрежем символ '/', если подкаталог

  len = strlen(ftpprop.work_dir);

  if (len>1)

  {

    if(ftpprop.work_dir[len-1]=='/')

    {

      ftpprop.work_dir[len-1]=0;

    }

  }

}

break;

Затем выходим из этого условия (не из цикла, а из условия совсем и обработаем все остальные условия

  

      ftpprop.work_dir[len-1]=0;

    }

  }

}

else

{

  //дополним путь символом '/' в конце, если его нет

  len = strlen(ftpprop.work_dir);

  if(ftpprop.work_dir[len-1]!='/') strcat(ftpprop.work_dir,"/");

  strcat(ftpprop.work_dir, arg);

}

break;

Здесь мы наоборот дополним имя слешем.

Затем выходим из этого тела и передаём клиенту ответ на сообщение

  strcat(ftpprop.work_dir, arg);

}

len = sprintf((char*)datasect->data, "250 CWD successful. "%s" is current directory.rn", ftpprop.work_dir);

tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

break;

Прошьём контроллер, собрав предварительно код и проверим, то, что команду клиента сервер обработал

Image13

Команда была принята клиентом, на что клиент среагировал, послав теперь не PWD, а другую команду TYPE. Это просьба клиента, чтобы сервер работал с клиентом в стандарте ASCII. Об этом говорит аргумент A. Если бы, например аргумент был бы B, то это означало бы просьбу клиента применять бинарный стандарт.

Также в Total Commander мы уже видим, что будто бы у нас уже существует соединение по протоколу FTP, но пока никакого списка не отображается

Image14

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

А пока же обработаем команду клиента

  break;

case TYPE_CMD :

  len = strlen(arg);

  arg[len - 1] = 0x00;

  arg[len - 2] = 0x00;

  switch(arg[0])

  {

    case 'A':

    case 'a': /* Ascii */

      ftpprop.ftp_type = ASCII_TYPE;

      len = sprintf((char*)datasect->data, "200 Type set to %srn", arg);

      tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

      break;

    case 'B':

    case 'b': /* Binary */

    case 'I':

    case 'i': /* Image */

      ftpprop.ftp_type = IMAGE_TYPE;

      len = sprintf((char*)datasect->data, "200 Type set to %srn", arg);

      tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

      break;

    default: /* Invalid */

      len = sprintf((char*)datasect->data, "501 Unknown type "%s"rn", arg);

      tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

      break;

  }

  break;

default: // Invalid

Тут хоть код и немалый, но объяснять-то, собственно в нём нечего. Просто реагируем на возможные аргументы и шлём соответствующие сообщения.

Соберём код, прошьём контроллер и посмотрим, что дальше запросит клиент

Image15

Мы видим, что сообщение наше клиент получил и теперь он посылает нам команду PASV. Данная команда обозначает просьбу клиента использовать пассивный режим обмена, так как мы устанавливали соответствующий чекбокс в настройках соединения FTP у клиента. Если сервер согласен на такой режим обмена, то он обязан ответить IP-адресом и портом соединения для передачи данных, так как мы уже знаем, что для передачи данных соединение будет отдельное. Так как команда данная для сервера ещё неизвестна, то сервер так и говорит, на что клиент не успокаивается и предлагает свои варианты соединения.

В следующей части занятия мы напишем код для вывода клиенту информации о каталоге.

 

 

Предыдущая часть Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь Nucleo STM32F401RE

и здесь Nucleo STM32F401RE

Ethernet LAN Сетевой Модуль можно купить здесь W5500 Ethernet LAN

Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

STM LAN. W5500. FTP Server

4 комментария на “STM Урок 95. LAN. W5500. FTP Server. Часть 2
  1. MDBK:

    You can do FTP Client . 

  2. Баха:

    Добрый день подскажите где можно взять фаил сгенерированый CubeMX (.ioc) для примеров из System Workbench for STM32 например из папки Applicatins. Или как его самому сгенерировать?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*