|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an extension to the Python FTP class to support FTP |
|
8 proxies. |
|
9 """ |
|
10 |
|
11 import enum |
|
12 import ftplib # secok |
|
13 from socket import _GLOBAL_DEFAULT_TIMEOUT |
|
14 |
|
15 |
|
16 class EricFtpProxyError(ftplib.Error): |
|
17 """ |
|
18 Class to signal an error related to proxy configuration. |
|
19 |
|
20 The error message starts with a three digit error code followed by a |
|
21 space and the error string. Supported error codes are: |
|
22 <ul> |
|
23 <li>910: proxy error; the second number gives the category of the proxy |
|
24 error. The original response from the proxy is appended in the next |
|
25 line.</li> |
|
26 <li>930: proxy error; the second number gives the category of the proxy |
|
27 error. The original response from the proxy is appended in the next |
|
28 line.</li> |
|
29 <li>940: proxy error; the second number gives the category of the proxy |
|
30 error. The original response from the proxy is appended in the next |
|
31 line.</li> |
|
32 <li>950: proxy error; the second number gives the category of the proxy |
|
33 error. The original response from the proxy is appended in the next |
|
34 line.</li> |
|
35 <li>990: proxy usage is enabled but no proxy host given</li> |
|
36 <li>991: proxy usage is enabled but no proxy user given</li> |
|
37 <li>992: proxy usage is enabled but no proxy password given</li> |
|
38 </ul> |
|
39 """ |
|
40 pass |
|
41 |
|
42 |
|
43 class EricFtpProxyType(enum.Enum): |
|
44 """ |
|
45 Class defining the supported FTP proxy types. |
|
46 """ |
|
47 NO_PROXY = 0 # no proxy |
|
48 NON_AUTHORIZING = 1 # non authorizing proxy |
|
49 USER_SERVER = 2 # proxy login first, than user@remote.host |
|
50 SITE = 3 # proxy login first, than use SITE command |
|
51 OPEN = 4 # proxy login first, than use OPEN command |
|
52 USER_PROXYUSER_SERVER = 5 # one login for both |
|
53 PROXYUSER_SERVER = 6 |
|
54 # proxy login with remote host given, than normal remote login |
|
55 AUTH_RESP = 7 # authenticate to proxy with AUTH and RESP commands |
|
56 BLUECOAT = 8 # bluecoat proxy |
|
57 |
|
58 |
|
59 class EricFtp(ftplib.FTP): |
|
60 """ |
|
61 Class implementing an extension to the Python FTP class to support FTP |
|
62 proxies. |
|
63 """ |
|
64 def __init__(self, host="", user="", password="", acct="", # secok |
|
65 proxyType=EricFtpProxyType.NO_PROXY, proxyHost="", |
|
66 proxyPort=ftplib.FTP_PORT, proxyUser="", proxyPassword="", |
|
67 proxyAccount="", timeout=_GLOBAL_DEFAULT_TIMEOUT): |
|
68 """ |
|
69 Constructor |
|
70 |
|
71 @param host name of the FTP host |
|
72 @type str |
|
73 @param user user name for login to FTP host |
|
74 @type str |
|
75 @param password password for login to FTP host |
|
76 @type str |
|
77 @param acct account for login to FTP host |
|
78 @type str |
|
79 @param proxyType type of the FTP proxy |
|
80 @type EricFtpProxyType |
|
81 @param proxyHost name of the FTP proxy |
|
82 @type str |
|
83 @param proxyPort port of the FTP proxy |
|
84 @type int |
|
85 @param proxyUser user name for login to the proxy |
|
86 @type str |
|
87 @param proxyPassword password for login to the proxy |
|
88 @type str |
|
89 @param proxyAccount accounting info for the proxy |
|
90 @type str |
|
91 @param timeout timeout in seconds for blocking operations |
|
92 @type int |
|
93 """ |
|
94 super().__init__() |
|
95 |
|
96 self.__timeout = timeout |
|
97 |
|
98 self.__proxyType = proxyType |
|
99 self.__proxyHost = proxyHost |
|
100 self.__proxyPort = proxyPort |
|
101 self.__proxyUser = proxyUser |
|
102 self.__proxyPassword = proxyPassword |
|
103 self.__proxyAccount = proxyAccount |
|
104 |
|
105 self.__host = host |
|
106 self.__port = ftplib.FTP_PORT |
|
107 self.__user = user |
|
108 self.__password = password |
|
109 self.__acct = acct |
|
110 |
|
111 if host: |
|
112 self.connect(host) |
|
113 if user: |
|
114 self.login(user, password, acct) |
|
115 |
|
116 def setProxy(self, proxyType=EricFtpProxyType.NO_PROXY, proxyHost="", |
|
117 proxyPort=ftplib.FTP_PORT, proxyUser="", proxyPassword="", |
|
118 proxyAccount=""): |
|
119 """ |
|
120 Public method to set the proxy configuration. |
|
121 |
|
122 @param proxyType type of the FTP proxy |
|
123 @type EricFtpProxyType |
|
124 @param proxyHost name of the FTP proxy |
|
125 @type str |
|
126 @param proxyPort port of the FTP proxy |
|
127 @type int |
|
128 @param proxyUser user name for login to the proxy |
|
129 @type str |
|
130 @param proxyPassword password for login to the proxy |
|
131 @type str |
|
132 @param proxyAccount accounting info for the proxy |
|
133 @type str |
|
134 """ |
|
135 self.__proxyType = proxyType |
|
136 self.__proxyHost = proxyHost |
|
137 self.__proxyPort = proxyPort |
|
138 self.__proxyUser = proxyUser |
|
139 self.__proxyPassword = proxyPassword |
|
140 self.__proxyAccount = proxyAccount |
|
141 |
|
142 def setProxyAuthentication(self, proxyUser="", proxyPassword="", |
|
143 proxyAccount=""): |
|
144 """ |
|
145 Public method to set the proxy authentication info. |
|
146 |
|
147 @param proxyUser user name for login to the proxy |
|
148 @type str |
|
149 @param proxyPassword password for login to the proxy |
|
150 @type str |
|
151 @param proxyAccount accounting info for the proxy |
|
152 @type str |
|
153 """ |
|
154 self.__proxyUser = proxyUser |
|
155 self.__proxyPassword = proxyPassword |
|
156 self.__proxyAccount = proxyAccount |
|
157 |
|
158 def connect(self, host="", port=0, timeout=-999): |
|
159 """ |
|
160 Public method to connect to the given FTP server. |
|
161 |
|
162 This extended method connects to the proxy instead of the given host, |
|
163 if a proxy is to be used. It throws an exception, if the proxy data |
|
164 is incomplete. |
|
165 |
|
166 @param host name of the FTP host |
|
167 @type str |
|
168 @param port port of the FTP host |
|
169 @type int |
|
170 @param timeout timeout in seconds for blocking operations |
|
171 @type int |
|
172 @return welcome message of the server |
|
173 @rtype str |
|
174 @exception EricFtpProxyError raised to indicate a proxy related issue |
|
175 """ |
|
176 if host: |
|
177 self.__host = host |
|
178 if port: |
|
179 self.__port = port |
|
180 if timeout != -999: |
|
181 self.__timeout = timeout |
|
182 |
|
183 if self.__proxyType != EricFtpProxyType.NO_PROXY: |
|
184 if not self.__proxyHost: |
|
185 raise EricFtpProxyError( |
|
186 "990 Proxy usage requested, but no proxy host given.") |
|
187 |
|
188 return super().connect( |
|
189 self.__proxyHost, self.__proxyPort, self.__timeout) |
|
190 else: |
|
191 return super().connect( |
|
192 self.__host, self.__port, self.__timeout) |
|
193 |
|
194 def login(self, user="", password="", acct=""): # secok |
|
195 """ |
|
196 Public method to login to the FTP server. |
|
197 |
|
198 This extended method respects the FTP proxy configuration. There are |
|
199 many different FTP proxy products available. But unfortunately there |
|
200 is no standard for how o traverse a FTP proxy. The lis below shows |
|
201 the sequence of commands used. |
|
202 |
|
203 <table> |
|
204 <tr><td>user</td><td>Username for remote host</td></tr> |
|
205 <tr><td>pass</td><td>Password for remote host</td></tr> |
|
206 <tr><td>pruser</td><td>Username for FTP proxy</td></tr> |
|
207 <tr><td>prpass</td><td>Password for FTP proxy</td></tr> |
|
208 <tr><td>remote.host</td><td>Hostname of the remote FTP server</td> |
|
209 </tr> |
|
210 </table> |
|
211 |
|
212 <dl> |
|
213 <dt>EricFtpProxyType.NO_PROXY:</dt> |
|
214 <dd> |
|
215 USER user<br/> |
|
216 PASS pass |
|
217 </dd> |
|
218 <dt>EricFtpProxyType.NON_AUTHORIZING:</dt> |
|
219 <dd> |
|
220 USER user@remote.host<br/> |
|
221 PASS pass |
|
222 </dd> |
|
223 <dt>EricFtpProxyType.USER_SERVER:</dt> |
|
224 <dd> |
|
225 USER pruser<br/> |
|
226 PASS prpass<br/> |
|
227 USER user@remote.host<br/> |
|
228 PASS pass |
|
229 </dd> |
|
230 <dt>EricFtpProxyType.SITE:</dt> |
|
231 <dd> |
|
232 USER pruser<br/> |
|
233 PASS prpass<br/> |
|
234 SITE remote.site<br/> |
|
235 USER user<br/> |
|
236 PASS pass |
|
237 </dd> |
|
238 <dt>EricFtpProxyType.OPEN:</dt> |
|
239 <dd> |
|
240 USER pruser<br/> |
|
241 PASS prpass<br/> |
|
242 OPEN remote.site<br/> |
|
243 USER user<br/> |
|
244 PASS pass |
|
245 </dd> |
|
246 <dt>EricFtpProxyType.USER_PROXYUSER_SERVER:</dt> |
|
247 <dd> |
|
248 USER user@pruser@remote.host<br/> |
|
249 PASS pass@prpass |
|
250 </dd> |
|
251 <dt>EricFtpProxyType.PROXYUSER_SERVER:</dt> |
|
252 <dd> |
|
253 USER pruser@remote.host<br/> |
|
254 PASS prpass<br/> |
|
255 USER user<br/> |
|
256 PASS pass |
|
257 </dd> |
|
258 <dt>EricFtpProxyType.AUTH_RESP:</dt> |
|
259 <dd> |
|
260 USER user@remote.host<br/> |
|
261 PASS pass<br/> |
|
262 AUTH pruser<br/> |
|
263 RESP prpass |
|
264 </dd> |
|
265 <dt>EricFtpProxyType.BLUECOAT:</dt> |
|
266 <dd> |
|
267 USER user@remote.host pruser<br/> |
|
268 PASS pass<br/> |
|
269 ACCT prpass |
|
270 </dd> |
|
271 </dl> |
|
272 |
|
273 @param user username for the remote host |
|
274 @type str |
|
275 @param password password for the remote host |
|
276 @type str |
|
277 @param acct accounting information for the remote host |
|
278 @type str |
|
279 @return response sent by the remote host |
|
280 @rtype str |
|
281 @exception EricFtpProxyError raised to indicate a proxy related issue |
|
282 @exception ftplib.error_reply raised to indicate an FTP error reply |
|
283 """ |
|
284 if not user: |
|
285 user = "anonymous" |
|
286 if not password: |
|
287 # make sure it is a string |
|
288 password = "" # secok |
|
289 if not acct: |
|
290 # make sure it is a string |
|
291 acct = "" |
|
292 if user == "anonymous" and password in {'', '-'}: |
|
293 password += "anonymous@" |
|
294 |
|
295 if self.__proxyType != EricFtpProxyType.NO_PROXY: |
|
296 if self.__proxyType != EricFtpProxyType.NON_AUTHORIZING: |
|
297 # check, if a valid proxy configuration is known |
|
298 if not self.__proxyUser: |
|
299 raise EricFtpProxyError( |
|
300 "991 Proxy usage requested, but no proxy user given") |
|
301 if not self.__proxyPassword: |
|
302 raise EricFtpProxyError( |
|
303 "992 Proxy usage requested, but no proxy password" |
|
304 " given") |
|
305 |
|
306 if self.__proxyType in [EricFtpProxyType.NON_AUTHORIZING, |
|
307 EricFtpProxyType.AUTH_RESP, |
|
308 EricFtpProxyType.BLUECOAT]: |
|
309 user += "@" + self.__host |
|
310 if self.__proxyType == EricFtpProxyType.BLUECOAT: |
|
311 user += " " + self.__proxyUser |
|
312 acct = self.__proxyPassword |
|
313 elif self.__proxyType == EricFtpProxyType.USER_PROXYUSER_SERVER: |
|
314 user = "{0}@{1}@{2}".format( |
|
315 user, self.__proxyUser, self.__host) |
|
316 password = "{0}@{1}".format(password, self.__proxyPassword) |
|
317 else: |
|
318 pruser = self.__proxyUser |
|
319 if self.__proxyType == EricFtpProxyType.USER_SERVER: |
|
320 user += "@" + self.__host |
|
321 elif self.__proxyType == EricFtpProxyType.PROXYUSER_SERVER: |
|
322 pruser += "@" + self.__host |
|
323 |
|
324 # authenticate to the proxy first |
|
325 presp = self.sendcmd("USER " + pruser) |
|
326 if presp[0] == "3": |
|
327 presp = self.sendcmd("PASS " + self.__proxyPassword) |
|
328 if presp[0] == "3" and self.__proxyAccount: |
|
329 presp = self.sendcmd("ACCT " + self.__proxyAccount) |
|
330 if presp[0] != "2": |
|
331 raise EricFtpProxyError( |
|
332 "9{0}0 Error authorizing at proxy\n{1}".format( |
|
333 presp[0], presp)) |
|
334 |
|
335 if self.__proxyType == EricFtpProxyType.SITE: |
|
336 # send SITE command |
|
337 presp = self.sendcmd("SITE " + self.__host) |
|
338 if presp[0] != "2": |
|
339 raise EricFtpProxyError( |
|
340 "9{0}0 Error sending SITE command\n{1}".format( |
|
341 presp[0], presp)) |
|
342 elif self.__proxyType == EricFtpProxyType.OPEN: |
|
343 # send OPEN command |
|
344 presp = self.sendcmd("OPEN " + self.__host) |
|
345 if presp[0] != "2": |
|
346 raise EricFtpProxyError( |
|
347 "9{0}0 Error sending OPEN command\n{1}".format( |
|
348 presp[0], presp)) |
|
349 |
|
350 # authenticate to the remote host or combined to proxy and remote host |
|
351 resp = self.sendcmd("USER " + user) |
|
352 if resp[0] == "3": |
|
353 resp = self.sendcmd("PASS " + password) |
|
354 if resp[0] == "3": |
|
355 resp = self.sendcmd("ACCT " + acct) |
|
356 if resp[0] != "2": |
|
357 raise ftplib.error_reply(resp) # secok |
|
358 |
|
359 if self.__proxyType == EricFtpProxyType.AUTH_RESP: |
|
360 # authorize to the FTP proxy |
|
361 presp = self.sendcmd("AUTH " + self.__proxyUser) |
|
362 if presp[0] == "3": |
|
363 presp = self.sendcmd("RESP " + self.__proxyPassword) |
|
364 if presp[0] != "2": |
|
365 raise EricFtpProxyError( |
|
366 "9{0}0 Error authorizing at proxy\n{1}".format( |
|
367 presp[0], presp)) |
|
368 |
|
369 return resp |