Меняйте каталоги, перечисляйте их содержимое и скачивайте файлы
В части 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, но я надеюсь, что это мягкое введение демистифицировало протокол передачи файлов и дало вам достаточную отправную точку для расширения вашего сервера. в свободное время.