Неявная проблема с TLS-соединением в Python FTP
У меня есть необходимость подключиться к серверу FTPS, к которому я могу успешно подключиться с помощью lftp. Однако, когда я пытаюсь с Python ftplib.FTP_TLS, он истекает, трассировка стека показывает, что он ждет, пока сервер отправит приветственное сообщение или что-то подобное. Кто-нибудь знает в чем проблема и как ее преодолеть? Интересно, нужно ли что-то делать на стороне сервера, но почему клиент lftp работает нормально? Любая помощь с благодарностью.
Вот трассировка стека:
ftp = ftplib.FTP_TLS()
ftp.connect(cfg.HOST, cfg.PORT, timeout=60)
File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 135, in connect
self.welcome = self.getresp()
File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 210, in getresp
resp = self.getmultiline()
File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 196, in getmultiline
line = self.getline()
File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 183, in getline
line = self.file.readline()
File "C:\Users\username\Softwares\Python27\lib\socket.py", line 447, in readline
data = self._sock.recv(self._rbufsize)
socket.timeout: timed out
Успешный вход в систему с использованием lftp на тот же FTP-сервер:
$ lftp
lftp :~> open ftps://ip_address:990
lftp ip_address:~> set ftps:initial-prot P
lftp ip_address:~> login ftps_user_id ftps_user_passwd
lftp sftp_user_id@ip_address:~> ls
ls: Fatal error: SSL_connect: self signed certificate
lftp ftps_user_id@ip_address:~> set ssl:verif-certificate off
lftp ftps_user_id@ip_address:~> ls
lftp ftps_user_id@ip_address:/>
Кстати, я использую Python 2.7.3. Я довольно много поискал с помощью Google, но не нашел ничего полезного.
У меня все еще есть эта проблема, спасибо, если кто-то может помочь. При внимательном рассмотрении FTP.connect() соединение с сервером не является проблемой, но получение подтверждения (или приветственного сообщения) от сервера является проблемой. У lftp нет этой проблемы, и FileZilla также не имеет проблем, как в журнале здесь -
Status: Connecting to xx.xx.xx.xxx:990...
Status: Connection established, initializing TLS...
Status: Verifying certificate...
Status: TLS/SSL connection established, waiting for welcome message...
Response: 220- Vous allez vous connecter sur un serveur prive
Response: 220- Seules les personnes habilitees y sont autorisees
Response: 220 Les contrevenants s'exposent aux poursuites prevues par la loi.
Command: USER xxxxxxxxxxxxx
Response: 331 Password required for xxxxxxxxxxxxx.
Command: PASS **********
Response: 230 Login OK. Proceed.
Command: PBSZ 0
Response: 200 PBSZ Command OK. Protection buffer size set to 0.
Command: PROT P
Response: 200 PROT Command OK. Using Private data connection
Status: Connected
Status: Retrieving directory listing...
Command: PWD
Response: 257 "/" is current folder.
Command: TYPE I
Response: 200 Type set to I.
Command: PASV
Response: 227 Entering Passive Mode (81,93,20,199,4,206).
Command: MLSD
Response: 150 Opening BINARY mode data connection for MLSD /.
Response: 226 Transfer complete. 0 bytes transferred. 0 bps.
Status: Directory listing successful
5 ответов
Расширяя предложенные решения, проблема заключается в том, что неявные соединения FTPS требуют, чтобы сокет был автоматически обернут ssl, прежде чем мы получим возможность вызвать login(). Многие подклассы, которые предлагают люди, делают это в контексте метода connect, мы можем более широко управлять этим, изменяя get/set self.sock со свойством auto-wrap on set:
import ftplib
import ssl
class ImplicitFTP_TLS(ftplib.FTP_TLS):
"""FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sock = None
@property
def sock(self):
"""Return the socket."""
return self._sock
@sock.setter
def sock(self, value):
"""When modifying the socket, ensure that it is ssl wrapped."""
if value is not None and not isinstance(value, ssl.SSLSocket):
value = self.context.wrap_socket(value)
self._sock = value
Использование по существу такое же, как со стандартным классом FTP_TLS:
ftp_client = ImplicitFTP_TLS()
ftp_client.connect(host='ftp.example.com', port=990)
ftp_client.login(user='USERNAME', passwd='PASSWORD')
ftp_client.prot_p()
Я работал над той же проблемой в течение полдня и наконец понял это.
Для неявного FTP TLS/SSL(порт defualt 990) наша клиентская программа должна создать соединение TLS / SSL сразу после создания сокета. Но класс питона FTP_TLS
не перезагружает функцию подключения из класса FTP. Нам нужно это починить:
class tyFTP(ftplib.FTP_TLS):
def __init__(self,
host='',
user='',
passwd='',
acct='',
keyfile=None,
certfile=None,
timeout=60):
ftplib.FTP_TLS.__init__(self,
host=host,
user=user,
passwd=passwd,
acct=acct,
keyfile=keyfile,
certfile=certfile,
timeout=timeout)
def connect(self, host='', port=0, timeout=-999):
"""Connect to host. Arguments are:
- host: hostname to connect to (string, default previous host)
- port: port to connect to (integer, default previous port)
"""
if host != '':
self.host = host
if port > 0:
self.port = port
if timeout != -999:
self.timeout = timeout
try:
self.sock = socket.create_connection((self.host, self.port), self.timeout)
self.af = self.sock.family
# add this line!!!
self.sock = ssl.wrap_socket(self.sock,
self.keyfile,
self.certfile,
ssl_version=ssl.PROTOCOL_TLSv1)
# add end
self.file = self.sock.makefile('rb')
self.welcome = self.getresp()
except Exception as e:
print(e)
return self.welcome
Этот производный класс перезагружает функцию соединения и создает оболочку вокруг сокета в TLS. После успешного подключения и входа на FTP-сервер вам необходимо позвонить: FTP_TLS.prot_p()
перед выполнением любой команды FTP!
Надеюсь, это поможет ^_^
Неявный FTP через TLS с пассивным режимом передачи
Вот реализация немного более "промышленная".
Заметим, что в предыдущих примерах в init было пропущено свойство с именем context.
Код ниже прекрасно работает как с Python 2.7, так и с Python 3
Код
import ftplib, socket, ssl
FTPTLS_OBJ = ftplib.FTP_TLS
# Class to manage implicit FTP over TLS connections, with passive transfer mode
# - Important note:
# If you connect to a VSFTPD server, check that the vsftpd.conf file contains
# the property require_ssl_reuse=NO
class FTPTLS(FTPTLS_OBJ):
host = "127.0.0.1"
port = 990
user = "anonymous"
timeout = 60
logLevel = 0
# Init both this and super
def __init__(self, host=None, user=None, passwd=None, acct=None, keyfile=None, certfile=None, context=None, timeout=60):
FTPTLS_OBJ.__init__(self, host, user, passwd, acct, keyfile, certfile, context, timeout)
# Custom function: Open a new FTPS session (both connection & login)
def openSession(self, host="127.0.0.1", port=990, user="anonymous", password=None, timeout=60):
self.user = user
# connect()
ret = self.connect(host, port, timeout)
# prot_p(): Set up secure data connection.
try:
ret = self.prot_p()
if (self.logLevel > 1): self._log("INFO - FTPS prot_p() done: " + ret)
except Exception as e:
if (self.logLevel > 0): self._log("ERROR - FTPS prot_p() failed - " + str(e))
raise e
# login()
try:
ret = self.login(user=user, passwd=password)
if (self.logLevel > 1): self._log("INFO - FTPS login() done: " + ret)
except Exception as e:
if (self.logLevel > 0): self._log("ERROR - FTPS login() failed - " + str(e))
raise e
if (self.logLevel > 1): self._log("INFO - FTPS session successfully opened")
# Override function
def connect(self, host="127.0.0.1", port=990, timeout=60):
self.host = host
self.port = port
self.timeout = timeout
try:
self.sock = socket.create_connection((self.host, self.port), self.timeout)
self.af = self.sock.family
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
self.file = self.sock.makefile('r')
self.welcome = self.getresp()
if (self.logLevel > 1): self._log("INFO - FTPS connect() done: " + self.welcome)
except Exception as e:
if (self.logLevel > 0): self._log("ERROR - FTPS connect() failed - " + str(e))
raise e
return self.welcome
# Override function
def makepasv(self):
host, port = FTPTLS_OBJ.makepasv(self)
# Change the host back to the original IP that was used for the connection
host = socket.gethostbyname(self.host)
return host, port
# Custom function: Close the session
def closeSession(self):
try:
self.close()
if (self.logLevel > 1): self._log("INFO - FTPS close() done")
except Exception as e:
if (self.logLevel > 0): self._log("ERROR - FTPS close() failed - " + str(e))
raise e
if (self.logLevel > 1): self._log("INFO - FTPS session successfully closed")
# Private method for logs
def _log(self, msg):
# Be free here on how to implement your own way to redirect logs (e.g: to a console, to a file, etc.)
print(msg)
Пример использования
host = "www.myserver.com"
port = 990
user = "myUserId"
password = "myPassword"
myFtps = FTPTLS()
myFtps.logLevel = 2
myFtps.openSession(host, port, user, password)
print(myFtps.retrlines("LIST"))
myFtps.closeSession()
Пример вывода
INFO - FTPS connect() done: 220 (vsFTPd 3.0.2)
INFO - FTPS prot_p() done: 200 PROT now Private.
INFO - FTPS login() done: 230 Login successful.
INFO - FTPS session successfully opened
-rw------- 1 ftp ftp 86735 Mar 22 16:55 MyModel.yaml
-rw------- 1 ftp ftp 9298 Mar 22 16:55 MyData.csv
226 Directory send OK.
INFO - FTPS close() done
INFO - FTPS session successfully closed
В продолжение ответа NERV - который мне очень помог, вот как я смог решить мою проблему с неявным TLS-соединением через порт 990, требующим аутентификации.
Имя файла: ImplicitTLS.py
from ftplib import FTP_TLS
import socket
import ssl
class tyFTP(FTP_TLS):
def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, timeout=60):
FTP_TLS.__init__(self, host, user, passwd, acct, keyfile, certfile, timeout)
def connect(self, host='', port=0, timeout=-999):
if host != '':
self.host = host
if port > 0:
self.port = port
if timeout != -999:
self.timeout = timeout
try:
self.sock = socket.create_connection((self.host, self.port), self.timeout)
self.af = self.sock.family
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ssl_version=ssl.PROTOCOL_TLSv1)
self.file = self.sock.makefile('rb')
self.welcome = self.getresp()
except Exception as e:
print e
return self.welcome
Затем из моего основного приложения я сделал это:
from ImplicityTLS import tyFTP
server = tyFTP()
server.connect(host="xxxxx", port=990)
server.login(user="yyyy", passwd="fffff")
server.prot_p()
И все, я смог загрузить файлы и т. Д. Рекомендую обратиться к NERV.
Ответ NERV и пример Брэда Декера были действительно полезны. Слава им. Они спасли меня часами.
К сожалению, изначально это не сработало для меня.
В моем случае соединение работало, как только я удалил ssl_version
параметр из ssl.wrap_socket
метод. Кроме того, чтобы отправить любую команду на сервер, мне пришлось перезаписать ntransfercmd
метод из FTP_TLS
класс и удалить ssl_version
Параметр там тоже.
Вот код, который работал для меня:
from ftplib import FTP_TLS, FTP
import socket
import ssl
class IMPLICIT_FTP_TLS(FTP_TLS):
def __init__(self, host='', user='', passwd='', acct='', keyfile=None,
certfile=None, timeout=60):
FTP_TLS.__init__(self, host, user, passwd, acct, keyfile, certfile, timeout)
def connect(self, host='', port=0, timeout=-999):
'''Connect to host. Arguments are:
- host: hostname to connect to (string, default previous host)
- port: port to connect to (integer, default previous port)
'''
if host != '':
self.host = host
if port > 0:
self.port = port
if timeout != -999:
self.timeout = timeout
try:
self.sock = socket.create_connection((self.host, self.port), self.timeout)
self.af = self.sock.family
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
self.file = self.sock.makefile('rb')
self.welcome = self.getresp()
except Exception as e:
print (e)
return self.welcome
def ntransfercmd(self, cmd, rest=None):
conn, size = FTP.ntransfercmd(self, cmd, rest)
if self._prot_p:
conn = ssl.wrap_socket(conn, self.keyfile, self.certfile)
return conn, size
И обязательный образец:
>>> ftps = IMPLICIT_FTP_TLS()
>>> ftps.connect(host='your.ftp.host', port=990)
>>> ftps.login(user="your_user", passwd="your_passwd")
>>> ftps.prot_p()
>>> ftps.retrlines('LIST')
У меня также была такая же проблема с сервером FTP, использующим неявный TLS (также в пассивном режиме). Я столкнулся с несколькими ошибками:
450 TLS session of data connection has not resumed or the session does not match the control connection
с предложенными решениями. Для справки, мое решение, основанное на @George Leslie-Waksman:
class ImplicitFTP_TLS(ftplib.FTP_TLS):
"""
FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS.
Prefer explicit TLS whenever possible.
"""
def __init__(self, *args, **kwargs):
"""Initialise self."""
super().__init__(*args, **kwargs)
self._sock = None
@property
def sock(self):
"""Return the socket."""
return self._sock
@sock.setter
def sock(self, value):
"""When modifying the socket, ensure that it is SSL wrapped."""
if value is not None and not isinstance(value, ssl.SSLSocket):
value = self.context.wrap_socket(value)
self._sock = value
def ntransfercmd(self, cmd, rest=None):
"""Override the ntransfercmd method"""
conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
conn = self.sock.context.wrap_socket(
conn, server_hostname=self.host, session=self.sock.session
)
return conn, size
Новизна заключается в ntransfercmd
переопределения.
Затем он используется аналогично ftplib.FTP_TLS
(многочисленные примеры в предыдущих постах).
Переопределение connect
вместо использования sock
свойство / сеттер тоже работает.
Я знаю, что эта ветка довольно старая, и ftp не так популярен, как когда-то, но в любом случае, на случай, если это поможет кому-то, кому бы я хотел внести дополнительный вклад. Я столкнулся с подобной ситуацией, пытаясь подключиться к FTP-серверу, используя IMPLICIT (Порт 990) ftps в режиме PASSIVE. В этой ситуации сервер после согласования начального соединения обычно предоставляет новый IP-адрес хоста и порт, которые, вероятно, отличаются от тех, которые использовались для установления начального соединения, по которым предполагается фактическая передача данных. Ничего страшного, FTP-клиенты, включая python, могут справиться с этим, только этот конкретный сервер предоставлял немаршрутизируемый (вероятно, внутренний для брандмауэра) IP-адрес. Я заметил, что FileZilla подключена без проблем, но Python ftplib не может. Тогда я столкнулся с этой темой:
Как заменить не маршрутизируемый IP-адрес на адрес сервера на ftplib
что объяснило меня. Используя методологию Гжегожа Вежовецкого, я расширил метод, о котором говорилось в этой теме, и придумал это, что решило мою проблему.
import ftplib, os, sys
import socket
import ssl
FTPS_OBJ = ftplib.FTP_TLS
def conn_i_ftps(FTP_Site, Login_Name, Login_Password):
print "Starting IMPLICIT ftp_tls..."
ftps = tyFTP()
print ftps.connect(host=FTP_Site, port=990, timeout=120)
ftps.prot_p()
ftps.login(user=Login_Name, passwd=Login_Password)
print "Logged In"
ftps.retrlines('LIST')
# return ftps
class tyFTP(FTPS_OBJ):
def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, timeout=60):
FTPS_OBJ.__init__(self, host, user, passwd, acct, keyfile, certfile, timeout)
def connect(self, host='', port=0, timeout=-999):
if host != '':
self.host = host
if port > 0:
self.port = port
if timeout != -999:
self.timeout = timeout
try:
self.sock = socket.create_connection((self.host, self.port), self.timeout)
self.af = self.sock.family
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
self.file = self.sock.makefile('rb')
self.welcome = self.getresp()
except Exception as e:
print e
return self.welcome
def makepasv(self):
print port #<---Show passively assigned port
print host #<---Show the non-routable, passively assigned IP
host, port = FTPS_OBJ.makepasv(self)
host = socket.gethostbyname(self.host) #<---- This changes the host back to the original IP that was used for the connection
print 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
print host #<----Showing the original IP
return host, port
Затем код вызывается так
FTP_Site = "ftp.someserver.com"
Login_Name = "some_name"
Login_Password = "some_passwd"
conn_i_ftps(FTP_Site, Login_Name, Login_Password)
Я полагаю, что можно обернуть строки, где хост меняется, с помощью оператора IF, идентифицирующего немаршрутизируемые адреса, вроде этого:
if host.split(".")[0] in (10, 192, 172):
host = socket.gethostbyname(self.host)
.
.
.