Меняйте каталоги, перечисляйте их содержимое и скачивайте файлы

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

Напоминаем, что это ftp.Serve, маршрутизатор, который отправляет команды от клиента их функциям-обработчикам. Именно эти функции-обработчики мы и будем изучать сегодня.

ПОЛЬЗОВАТЕЛЬ

Первое, что делает FTP-клиент, когда он устанавливает соединение с сервером, - это предоставляет информацию о пользователе. Стандартный протокол FTP, в отличие от SFTP (Secure FTP), полагается на базовую передачу незашифрованных имен пользователей и паролей, которые уязвимы для перехвата паролей и атак типа «злоумышленник в середине».

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

Фактически, мы зайдем так далеко, что разрешим анонимный доступ к нашему FTP-серверу, слепо доверяя любому, кто подключается, не идти туда, куда им не следует, и красть конфиденциальные документы. Ужасная политика, но она позволит нам быстро установить скелет для всего сервера и улучшить по вашему выбору.

Когда ftp.Serve встречает команду USER, она передает аргументы методу обработчика ftp.Conn: (c *Conn) user. Отсюда легко представить, как данные будут проверяться по базе данных известных пользователей и их прав доступа. После аутентификации имя пользователя и привилегии могут быть сохранены в качестве дополнительных полей в экземпляре ftp.Conn.

В нашем случае мы настолько превозносим безопасность сети, что просто возвращаем имя пользователя клиенту с соответствующим кодом успеха: 230 User %s logged in, proceed.

На стороне клиента это будет выглядеть примерно так:

cd / CWD

Переключить каталог так же просто, как и необходимо. Когда вы отправляете команду, например cd ../parent_folder, своему FTP-клиенту, он отправляет это сообщение на сервер как CWD ../parent_folder. ftp.Serve передает путь к файлу cwd, метод нашей проприетарной ftp.Conn.

После проверки того, что у нас есть правильное количество аргументов, один, мы создаем «абсолютный» путь, который использует корневой каталог сервера в качестве своего корня. В простейшей форме это вопрос присоединения аргумента пути к концу текущего рабочего каталога, а затем присоединения результата к концу rootDir.

Однако прежде чем ответить, что изменение было успешным, нам необходимо убедиться, что каталог существует и доступен. Возможно, программа или, если вы расширили обработчик USER, пользователя этого Conn, не имеют разрешения на чтение целевого каталога.

Go's os.Stat возвращает информацию о целевом файле и ошибку, которая не равна нулю, если к файлу нет доступа. В этом случае мы регистрируем это и отвечаем 550 Requested action not taken. File unavailable.. В противном случае мы обновляем ftp.Conn workDir и отвечаем 200 сообщением об успешном завершении.

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

Кроме того, постоянное добавление новых аргументов пути к постоянно растущему workDir - ужасное управление памятью, которое также замедляет поиск файлов. Узнайте, как filepath.Clean стандартной библиотеки может помочь в решении этой проблемы.

ls / СПИСОК

Теперь, когда пользователи могут менять каталог, они хотят знать, что внутри. Клиентская ls path/to/file команда достигает сервера как LIST path/to/file, и неудивительно, что у нас есть (c *Conn) list функция-обработчик для сопоставления.

Сначала мы строим абсолютный путь из сервера rootDir, workDir соединения и необязательного аргумента пути к файлу, предоставленного для ls. Если пользователь не предоставил аргумент пути, мы перечисляем содержимое текущего workDir.

ioutil.ReadDir - чрезвычайно полезная функция, которая возвращает фрагмент os.FileInfo, описывающий каждый файл в каталоге, или ошибку, если целевая папка недоступна. Таким образом, мы можем проверить путь и получить FileInfo за одну операцию. Если путь правильный, мы выдаем первоначальный ответ: 150 File status okay; about to open data connection.

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

Как этого добиться? Перед отправкой директивы LIST на сервер клиент незаметно отправляет другую команду: PORT. PORT имеет шестибайтовый аргумент, соответствующий четырем частям IP-адреса, плюс два байта для представления номера порта длиной до пяти цифр, например, PORT [127,0,0,1,245,1]. Имейте в виду, что ваш клиент может использовать несколько другие форматы, но, если на вашем сервере есть надлежащее ведение журнала, вы скоро заметите разницу.

ftp.Serve имеет case для команды PORT, маршрутизирующей аргумент IP-адреса на (c *Conn) port, ниже.

Это механизм, с помощью которого мы устанавливаем последнее отсутствующее поле на ftp.Conn, dataPort. dataPortFromHostPort анализирует шестибайтовый формат IP-адреса в структуру его частей, которые мы храним в экземпляре ftp.Conn. toAddress преобразует эту структуру в традиционно форматированный IP-адрес-плюс-порт, к которому сервер может подключиться с помощью net.Dial.

Как создать номер порта длиной до пяти цифр из двух отдельных байтов? p1<<8 + p2. Начните со сдвига p1 влево на восемь позиций. Если p1 = 00011011, p1<<8 = 0001101100000000. Когда вы добавляете p2, он заполняет восемь битов, оставшихся пустыми при сдвиге. p2 = 11111111; p1 + p2 = 0001101111111111 = 7167.

Как только у нас есть IP-адрес, предоставленный клиентом, соединение для передачи данных устанавливается с помощью вызова (c *Conn) dataConnect, простой оболочки для net.Dial. Обратите внимание, что dataConnect возвращает структуру, удовлетворяющую интерфейсу net.Conn (net.TCPConn), а не нашему пользовательскому ftp.Conn.

Как и в случае с нашим основным клиентским подключением, мы должны не забыть закрыть его после завершения, что потребует defer dataConn.Close(). С этого момента перечисление содержимого каталога представляет собой цикл по фрагменту os.FileInfo, запись каждого имени файла в dataConn и применение правильных терминаторов строки с EOL. Подведем итоги второй части list:

А вот результат, который вы можете ожидать при запуске ls в каталоге, содержащем файлы test.txt и test_img.png:

получить / RETR

То, чего мы все ждали: загрузка файлов с сервера. Теперь, когда вы узнали, как построить путь к файлу из корня сервера и открыть соединение для передачи данных, вас больше ничего не удивляет, кроме того факта, что команда get, которую вы вводите в своем клиенте, отправляется на сервер как RETR.

На этот раз вместо получения FileInfo для каталога с ioutil.ReadDir мы пытаемся открыть указанный файл для чтения с помощью os.Open. Если к файлу нет доступа, используйте основное соединение для отправки 550 Requested action not taken. File unavailable.

Если файл существует, мы открываем новое соединение для передачи данных с (c *Conn) dataConnect. Как и в случае с list, клиент перед каждым запросом RETR с командой PORT, поэтому поле dataPort ftp.Conn уже заполнено.

В этой базовой реализации используется io.Copy, чтобы загрузить весь файл в память и скопировать его непосредственно в соединение для передачи данных. Это хорошо работает для небольших файлов, но файлы в масштабе гигабайта могут убить сервер, особенно если учесть, что мы обслуживаем несколько клиентов одновременно. Подумайте, как можно уменьшить нагрузку на память на сервере, читая файлы построчно или в фиксированном количестве байтов.

Как всегда, мы завершаем передачу соответствующей строкой FTP, соответствующей типу данных, и сигнализируем об успешном завершении основного соединения.

Попробуйте; ваш результат должен выглядеть примерно так:

Вот и все - компактная и сверхлегкая реализация FTP-сервера на Go с большим пространством для разработки. В ftp.Serve есть команды, которые мы не обсуждали, такие как QUIT, и многие другие, описанные в спецификации FTP, но я надеюсь, что это мягкое введение демистифицировало протокол передачи файлов и дало вам достаточную отправную точку для расширения вашего сервера. в свободное время.