132
|
1 /*******************************************************************************
|
|
2
|
|
3 copyright: Copyright (c) 2006 UWB. All rights reserved
|
|
4
|
|
5 license: BSD style: $(LICENSE)
|
|
6
|
|
7 version: Initial release: June 2006
|
|
8
|
|
9 author: UWB
|
|
10
|
|
11 *******************************************************************************/
|
|
12
|
|
13 module tango.net.ftp.FtpClient;
|
|
14
|
|
15 private import tango.net.Socket;
|
|
16
|
|
17 private import tango.net.ftp.Telnet;
|
|
18
|
|
19 private import tango.time.Clock;
|
|
20
|
|
21 private import tango.io.Conduit,
|
|
22 tango.io.GrowBuffer,
|
|
23 tango.io.FileConduit;
|
|
24
|
|
25 private import tango.time.chrono.Gregorian;
|
|
26
|
|
27 private import Text = tango.text.Util;
|
|
28
|
|
29 private import Ascii = tango.text.Ascii;
|
|
30
|
|
31 private import Regex = tango.text.Regex;
|
|
32
|
|
33 private import Integer = tango.text.convert.Integer;
|
|
34
|
|
35 private import Timestamp = tango.text.convert.TimeStamp;
|
|
36
|
|
37
|
|
38 /// An FTP progress delegate.
|
|
39 ///
|
|
40 /// You may need to add the restart position to this, and use SIZE to determine
|
|
41 /// percentage completion. This only represents the number of bytes
|
|
42 /// transferred.
|
|
43 ///
|
|
44 /// Params:
|
|
45 /// pos = the current offset into the stream
|
|
46 alias void delegate(in size_t pos) FtpProgress;
|
|
47
|
|
48 /// The format of data transfer.
|
|
49 enum FtpFormat
|
|
50 {
|
|
51 /// Indicates ASCII NON PRINT format (line ending conversion to CRLF.)
|
|
52 ascii,
|
|
53 /// Indicates IMAGE format (8 bit binary octets.)
|
|
54 image,
|
|
55 }
|
|
56
|
|
57 /// A server response, consisting of a code and a potentially multi-line message.
|
|
58 struct FtpResponse
|
|
59 {
|
|
60 /// The response code.
|
|
61 ///
|
|
62 /// The digits in the response code can be used to determine status
|
|
63 /// programatically.
|
|
64 ///
|
|
65 /// First Digit (status):
|
|
66 /// 1xx = a positive, but preliminary, reply
|
|
67 /// 2xx = a positive reply indicating completion
|
|
68 /// 3xx = a positive reply indicating incomplete status
|
|
69 /// 4xx = a temporary negative reply
|
|
70 /// 5xx = a permanent negative reply
|
|
71 ///
|
|
72 /// Second Digit (subject):
|
|
73 /// x0x = condition based on syntax
|
|
74 /// x1x = informational
|
|
75 /// x2x = connection
|
|
76 /// x3x = authentication/process
|
|
77 /// x5x = file system
|
|
78 char[3] code = "000";
|
|
79
|
|
80 /// The message from the server.
|
|
81 ///
|
|
82 /// With some responses, the message may contain parseable information.
|
|
83 /// For example, this is true of the 257 response.
|
|
84 char[] message = null;
|
|
85 }
|
|
86
|
|
87 /// Active or passive connection mode.
|
|
88 enum FtpConnectionType
|
|
89 {
|
|
90 /// Active - server connects to client on open port.
|
|
91 active,
|
|
92 /// Passive - server listens for a connection from the client.
|
|
93 passive,
|
|
94 }
|
|
95
|
|
96 /// Detail about the data connection.
|
|
97 ///
|
|
98 /// This is used to properly send PORT and PASV commands.
|
|
99 struct FtpConnectionDetail
|
|
100 {
|
|
101 /// The type to be used.
|
|
102 FtpConnectionType type = FtpConnectionType.passive;
|
|
103
|
|
104 /// The address to give the server.
|
|
105 Address address = null;
|
|
106
|
|
107 /// The address to actually listen on.
|
|
108 Address listen = null;
|
|
109 }
|
|
110
|
|
111 /// A supported feature of an FTP server.
|
|
112 struct FtpFeature
|
|
113 {
|
|
114 /// The command which is supported, e.g. SIZE.
|
|
115 char[] command = null;
|
|
116 /// Parameters for this command; e.g. facts for MLST.
|
|
117 char[] params = null;
|
|
118 }
|
|
119
|
|
120 /// The type of a file in an FTP listing.
|
|
121 enum FtpFileType
|
|
122 {
|
|
123 /// An unknown file or type (no type fact.)
|
|
124 unknown,
|
|
125 /// A regular file, or similar.
|
|
126 file,
|
|
127 /// The current directory (e.g. ., but not necessarily.)
|
|
128 cdir,
|
|
129 /// A parent directory (usually "..".)
|
|
130 pdir,
|
|
131 /// Any other type of directory.
|
|
132 dir,
|
|
133 /// Another type of file. Consult the "type" fact.
|
|
134 other,
|
|
135 }
|
|
136
|
|
137 /// Information about a file in an FTP listing.
|
|
138 struct FtpFileInfo
|
|
139 {
|
|
140 /// The filename.
|
|
141 char[] name = null;
|
|
142 /// Its type.
|
|
143 FtpFileType type = FtpFileType.unknown;
|
|
144 /// Size in bytes (8 bit octets), or -1 if not available.
|
|
145 long size = -1;
|
|
146 /// Modification time, if available.
|
|
147 Time modify = Time.max;
|
|
148 /// Creation time, if available (not often.)
|
|
149 Time create = Time.max;
|
|
150 /// The file's mime type, if known.
|
|
151 char[] mime = null;
|
|
152 /// An associative array of all facts returned by the server, lowercased.
|
|
153 char[][char[]] facts;
|
|
154 }
|
|
155
|
|
156 /// A connection to an FTP server.
|
|
157 ///
|
|
158 /// Example:
|
|
159 /// ----------
|
|
160 /// auto ftp = new FTPConnection("hostname", "user", "pass",21);
|
|
161 ///
|
|
162 /// ftp.mkdir("test");
|
|
163 /// ftp.close();
|
|
164 /// ----------
|
|
165 ///
|
|
166 /// Standards: RFC 959, RFC 2228, RFC 2389, RFC 2428
|
|
167 ///
|
|
168 /// Bugs:
|
|
169 /// Does not support several uncommon FTP commands and responses.
|
|
170
|
|
171
|
|
172 class FTPConnection : Telnet
|
|
173 {
|
|
174 /// Supported features (if known.)
|
|
175 ///
|
|
176 /// This will be empty if not known, or else contain at least FEAT.
|
|
177 public FtpFeature[] supported_features = null;
|
|
178
|
|
179 /// Data connection information.
|
|
180 protected FtpConnectionDetail data_info;
|
|
181
|
|
182 /// The last-set restart position.
|
|
183 ///
|
|
184 /// This is only used when a local file is used for a RETR or STOR.
|
|
185 protected size_t restart_pos = 0;
|
|
186
|
|
187 /// error handler
|
|
188 protected void exception (char[] msg)
|
|
189 {
|
|
190 throw new FTPException ("Exception: " ~ msg);
|
|
191 }
|
|
192
|
|
193 /// ditto
|
|
194 protected void exception (FtpResponse r)
|
|
195 {
|
|
196 throw new FTPException (r);
|
|
197 }
|
|
198
|
|
199 /// Construct an FTPConnection without connecting immediately.
|
|
200 public this()
|
|
201 {
|
|
202 }
|
|
203
|
|
204 /// Connect to an FTP server with a username and password.
|
|
205 ///
|
|
206 /// Params:
|
|
207 /// hostname = the hostname or IP address to connect to
|
|
208 /// port = the port number to connect to
|
|
209 /// username = username to be sent
|
|
210 /// password = password to be sent, if requested
|
|
211 public this(char[] hostname, char[] username, char[] password, int port = 21)
|
|
212 {
|
|
213 this.connect(hostname, username, password,port);
|
|
214 }
|
|
215
|
|
216 /// Connect to an FTP server with a username and password.
|
|
217 ///
|
|
218 /// Params:
|
|
219 /// hostname = the hostname or IP address to connect to
|
|
220 /// port = the port number to connect to
|
|
221 /// username = username to be sent
|
|
222 /// password = password to be sent, if requested
|
|
223 public void connect(char[] hostname, char[] username, char[] password, int port = 21)
|
|
224 in
|
|
225 {
|
|
226 // We definitely need a hostname and port.
|
|
227 assert (hostname.length > 0);
|
|
228 assert (port > 0);
|
|
229 }
|
|
230 body
|
|
231 {
|
|
232 // Close any active connection.
|
|
233
|
|
234 if (this.socket !is null)
|
|
235 this.close();
|
|
236
|
|
237
|
|
238 // Connect to whichever FTP server responds first.
|
|
239 this.findAvailableServer(hostname, port);
|
|
240
|
|
241 this.socket.blocking = false;
|
|
242
|
|
243 scope (failure)
|
|
244 {
|
|
245 this.close();
|
|
246 }
|
|
247
|
|
248 // The welcome message should always be a 220. 120 and 421 are considered errors.
|
|
249 this.readResponse("220");
|
|
250
|
|
251 if (username.length == 0)
|
|
252 return;
|
|
253
|
|
254 // Send the username. Anything but 230, 331, or 332 is basically an error.
|
|
255 this.sendCommand("USER", username);
|
|
256 auto response = this.readResponse();
|
|
257
|
|
258 // 331 means username okay, please proceed with password.
|
|
259 if (response.code == "331")
|
|
260 {
|
|
261 this.sendCommand("PASS", password);
|
|
262 response = this.readResponse();
|
|
263 }
|
|
264
|
|
265 // We don't support ACCT (332) so we should get a 230 here.
|
|
266 if (response.code != "230" && response.code != "202")
|
|
267 {
|
|
268
|
|
269 exception (response);
|
|
270 }
|
|
271
|
|
272 }
|
|
273
|
|
274 /// Close the connection to the server.
|
|
275 public void close()
|
|
276 {
|
|
277 assert (this.socket !is null);
|
|
278
|
|
279 // Don't even try to close it if it's not open.
|
|
280 if (this.socket !is null)
|
|
281 {
|
|
282 try
|
|
283 {
|
|
284 this.sendCommand("QUIT");
|
|
285 this.readResponse("221");
|
|
286 }
|
|
287 // Ignore if the above could not be completed.
|
|
288 catch (FTPException)
|
|
289 {
|
|
290 }
|
|
291
|
|
292 // Shutdown the socket...
|
|
293 this.socket.shutdown(SocketShutdown.BOTH);
|
|
294 this.socket.detach();
|
|
295
|
|
296 // Clear out everything.
|
|
297 delete this.supported_features;
|
|
298 delete this.socket;
|
|
299 }
|
|
300 }
|
|
301
|
|
302 /// Set the connection to use passive mode for data tranfers.
|
|
303 ///
|
|
304 /// This is the default.
|
|
305 public void setPassive()
|
|
306 {
|
|
307 this.data_info.type = FtpConnectionType.passive;
|
|
308
|
|
309 delete this.data_info.address;
|
|
310 delete this.data_info.listen;
|
|
311 }
|
|
312
|
|
313 /// Set the connection to use active mode for data transfers.
|
|
314 ///
|
|
315 /// This may not work behind firewalls.
|
|
316 ///
|
|
317 /// Params:
|
|
318 /// ip = the ip address to use
|
|
319 /// port = the port to use
|
|
320 /// listen_ip = the ip to listen on, or null for any
|
|
321 /// listen_port = the port to listen on, or 0 for the same port
|
|
322 public void setActive(char[] ip, ushort port, char[] listen_ip = null, ushort listen_port = 0)
|
|
323 in
|
|
324 {
|
|
325 assert (ip.length > 0);
|
|
326 assert (port > 0);
|
|
327 }
|
|
328 body
|
|
329 {
|
|
330 this.data_info.type = FtpConnectionType.active;
|
|
331 this.data_info.address = new IPv4Address(ip, port);
|
|
332
|
|
333 // A local-side port?
|
|
334 if (listen_port == 0)
|
|
335 listen_port = port;
|
|
336
|
|
337 // Any specific IP to listen on?
|
|
338 if (listen_ip == null)
|
|
339 this.data_info.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port);
|
|
340 else
|
|
341 this.data_info.listen = new IPv4Address(listen_ip, listen_port);
|
|
342 }
|
|
343
|
|
344
|
|
345 /// Change to the specified directory.
|
|
346 public void cd(char[] dir)
|
|
347 in
|
|
348 {
|
|
349 assert (dir.length > 0);
|
|
350 }
|
|
351 body
|
|
352 {
|
|
353 this.sendCommand("CWD", dir);
|
|
354 this.readResponse("250");
|
|
355 }
|
|
356
|
|
357 /// Change to the parent of this directory.
|
|
358 public void cdup()
|
|
359 {
|
|
360 this.sendCommand("CDUP");
|
|
361 this.readResponse("200");
|
|
362 }
|
|
363
|
|
364 /// Determine the current directory.
|
|
365 ///
|
|
366 /// Returns: the current working directory
|
|
367 public char[] cwd()
|
|
368 {
|
|
369 this.sendCommand("PWD");
|
|
370 auto response = this.readResponse("257");
|
|
371
|
|
372 return this.parse257(response);
|
|
373 }
|
|
374
|
|
375 /// Change the permissions of a file.
|
|
376 ///
|
|
377 /// This is a popular feature of most FTP servers, but not explicitly outlined
|
|
378 /// in the spec. It does not work on, for example, Windows servers.
|
|
379 ///
|
|
380 /// Params:
|
|
381 /// path = the path to the file to chmod
|
|
382 /// mode = the desired mode; expected in octal (0777, 0644, etc.)
|
|
383 public void chmod(char[] path, int mode)
|
|
384 in
|
|
385 {
|
|
386 assert (path.length > 0);
|
|
387 assert (mode >= 0 && (mode >> 16) == 0);
|
|
388 }
|
|
389 body
|
|
390 {
|
|
391 char[] tmp = "000";
|
|
392 // Convert our octal parameter to a string.
|
|
393 Integer.format(tmp, cast(long) mode, Integer.Style.Octal);
|
|
394 this.sendCommand("SITE CHMOD", tmp, path);
|
|
395 this.readResponse("200");
|
|
396 }
|
|
397
|
|
398 /// Remove a file or directory.
|
|
399 ///
|
|
400 /// Params:
|
|
401 /// path = the path to the file or directory to delete
|
|
402 public void del(char[] path)
|
|
403 in
|
|
404 {
|
|
405 assert (path.length > 0);
|
|
406 }
|
|
407 body
|
|
408 {
|
|
409 this.sendCommand("DELE", path);
|
|
410 auto response = this.readResponse();
|
|
411
|
|
412 // Try it as a directory, then...?
|
|
413 if (response.code != "250")
|
|
414 this.rm(path);
|
|
415 }
|
|
416
|
|
417 /// Remove a directory.
|
|
418 ///
|
|
419 /// Params:
|
|
420 /// path = the directory to delete
|
|
421 public void rm(char[] path)
|
|
422 in
|
|
423 {
|
|
424 assert (path.length > 0);
|
|
425 }
|
|
426 body
|
|
427 {
|
|
428 this.sendCommand("RMD", path);
|
|
429 this.readResponse("250");
|
|
430 }
|
|
431
|
|
432 /// Rename/move a file or directory.
|
|
433 ///
|
|
434 /// Params:
|
|
435 /// old_path = the current path to the file
|
|
436 /// new_path = the new desired path
|
|
437 public void rename(char[] old_path, char[] new_path)
|
|
438 in
|
|
439 {
|
|
440 assert (old_path.length > 0);
|
|
441 assert (new_path.length > 0);
|
|
442 }
|
|
443 body
|
|
444 {
|
|
445 // Rename from... rename to. Pretty simple.
|
|
446 this.sendCommand("RNFR", old_path);
|
|
447 this.readResponse("350");
|
|
448
|
|
449 this.sendCommand("RNTO", new_path);
|
|
450 this.readResponse("250");
|
|
451 }
|
|
452
|
|
453 /// Determine the size in bytes of a file.
|
|
454 ///
|
|
455 /// This size is dependent on the current type (ASCII or IMAGE.)
|
|
456 ///
|
|
457 /// Params:
|
|
458 /// path = the file to retrieve the size of
|
|
459 /// format = what format the size is desired in
|
|
460 public size_t size(char[] path, FtpFormat format = FtpFormat.image)
|
|
461 in
|
|
462 {
|
|
463 assert (path.length > 0);
|
|
464 }
|
|
465 body
|
|
466 {
|
|
467 this.type(format);
|
|
468
|
|
469 this.sendCommand("SIZE", path);
|
|
470 auto response = this.readResponse("213");
|
|
471
|
|
472 // Only try to parse the numeric bytes of the response.
|
|
473 size_t end_pos = 0;
|
|
474 while (end_pos < response.message.length)
|
|
475 {
|
|
476 if (response.message[end_pos] < '0' || response.message[end_pos] > '9')
|
|
477 break;
|
|
478 end_pos++;
|
|
479 }
|
|
480
|
|
481 return toInt(response.message[0 .. end_pos]);
|
|
482 }
|
|
483
|
|
484 /// Send a command and process the data socket.
|
|
485 ///
|
|
486 /// This opens the data connection and checks for the appropriate response.
|
|
487 ///
|
|
488 /// Params:
|
|
489 /// command = the command to send (e.g. STOR)
|
|
490 /// parameters = any arguments to send
|
|
491 ///
|
|
492 /// Returns: the data socket
|
|
493 public Socket processDataCommand(char[] command, char[][] parameters ...)
|
|
494 {
|
|
495 // Create a connection.
|
|
496 Socket data = this.getDataSocket();
|
|
497 scope (failure)
|
|
498 {
|
|
499 // Close the socket, whether we were listening or not.
|
|
500 data.shutdown(SocketShutdown.BOTH);
|
|
501 data.detach();
|
|
502 }
|
|
503
|
|
504 // Tell the server about it.
|
|
505 this.sendCommand(command, parameters);
|
|
506
|
|
507 // We should always get a 150/125 response.
|
|
508 auto response = this.readResponse();
|
|
509 if (response.code != "150" && response.code != "125")
|
|
510 exception (response);
|
|
511
|
|
512 // We might need to do this for active connections.
|
|
513 this.prepareDataSocket(data);
|
|
514
|
|
515 return data;
|
|
516 }
|
|
517
|
|
518 /// Clean up after the data socket and process the response.
|
|
519 ///
|
|
520 /// This closes the socket and reads the 226 response.
|
|
521 ///
|
|
522 /// Params:
|
|
523 /// data = the data socket
|
|
524 public void finishDataCommand(Socket data)
|
|
525 {
|
|
526 // Close the socket. This tells the server we're done (EOF.)
|
|
527 data.shutdown(SocketShutdown.BOTH);
|
|
528 data.detach();
|
|
529
|
|
530 // We shouldn't get a 250 in STREAM mode.
|
|
531 this.readResponse("226");
|
|
532 }
|
|
533
|
|
534 /// Get a data socket from the server.
|
|
535 ///
|
|
536 /// This sends PASV/PORT as necessary.
|
|
537 ///
|
|
538 /// Returns: the data socket or a listener
|
|
539 protected Socket getDataSocket()
|
|
540 {
|
|
541 // What type are we using?
|
|
542 switch (this.data_info.type)
|
|
543 {
|
|
544 default:
|
|
545 exception ("unknown connection type");
|
|
546
|
|
547 // Passive is complicated. Handle it in another member.
|
|
548 case FtpConnectionType.passive:
|
|
549 return this.connectPassive();
|
|
550
|
|
551 // Active is simpler, but not as fool-proof.
|
|
552 case FtpConnectionType.active:
|
|
553 IPv4Address data_addr = cast(IPv4Address) this.data_info.address;
|
|
554
|
|
555 // Start listening.
|
|
556 Socket listener = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP);
|
|
557 listener.bind(this.data_info.listen);
|
|
558 listener.listen(32);
|
|
559
|
|
560 // Use EPRT if we know it's supported.
|
|
561 if (this.is_supported("EPRT"))
|
|
562 {
|
|
563 char[64] tmp = void;
|
|
564
|
|
565 this.sendCommand("EPRT", Text.layout(tmp, "|1|%0|%1|", data_addr.toAddrString, data_addr.toPortString));
|
|
566 // this.sendCommand("EPRT", format("|1|%s|%s|", data_addr.toAddrString(), data_addr.toPortString()));
|
|
567 this.readResponse("200");
|
|
568 }
|
|
569 else
|
|
570 {
|
|
571 int h1, h2, h3, h4, p1, p2;
|
|
572 h1 = (data_addr.addr() >> 24) % 256;
|
|
573 h2 = (data_addr.addr() >> 16) % 256;
|
|
574 h3 = (data_addr.addr() >> 8_) % 256;
|
|
575 h4 = (data_addr.addr() >> 0_) % 256;
|
|
576 p1 = (data_addr.port() >> 8_) % 256;
|
|
577 p2 = (data_addr.port() >> 0_) % 256;
|
|
578
|
|
579 // low overhead method to format a numerical string
|
|
580 char[64] tmp = void;
|
|
581 char[20] foo = void;
|
|
582 auto str = Text.layout (tmp, "%0,%1,%2,%3,%4,%5",
|
|
583 Integer.format(foo[0..3], h1),
|
|
584 Integer.format(foo[3..6], h2),
|
|
585 Integer.format(foo[6..9], h3),
|
|
586 Integer.format(foo[9..12], h4),
|
|
587 Integer.format(foo[12..15], p1),
|
|
588 Integer.format(foo[15..18], p2));
|
|
589
|
|
590 // This formatting is weird.
|
|
591 // this.sendCommand("PORT", format("%d,%d,%d,%d,%d,%d", h1, h2, h3, h4, p1, p2));
|
|
592
|
|
593 this.sendCommand("PORT", str);
|
|
594 this.readResponse("200");
|
|
595 }
|
|
596
|
|
597 return listener;
|
|
598 }
|
|
599 assert (false);
|
|
600 }
|
|
601
|
|
602 /// Prepare a data socket for use.
|
|
603 ///
|
|
604 /// This modifies the socket in some cases.
|
|
605 ///
|
|
606 /// Params:
|
|
607 /// data = the data listener socket
|
|
608 protected void prepareDataSocket(inout Socket data)
|
|
609 {
|
|
610 switch (this.data_info.type)
|
|
611 {
|
|
612 default:
|
|
613 exception ("unknown connection type");
|
|
614
|
|
615 case FtpConnectionType.active:
|
|
616 Socket new_data = null;
|
|
617
|
|
618 SocketSet set = new SocketSet();
|
|
619 scope (exit)
|
|
620 delete set;
|
|
621
|
|
622 // At end_time, we bail.
|
|
623 Time end_time = Clock.now + this.timeout;
|
|
624
|
|
625 while (Clock.now < end_time)
|
|
626 {
|
|
627 set.reset();
|
|
628 set.add(data);
|
|
629
|
|
630 // Can we accept yet?
|
|
631 int code = Socket.select(set, null, null, this.timeout);
|
|
632 if (code == -1 || code == 0)
|
|
633 break;
|
|
634
|
|
635 new_data = data.accept();
|
|
636 break;
|
|
637 }
|
|
638
|
|
639 if (new_data is null)
|
|
640 throw new FTPException("CLIENT: No connection from server", "420");
|
|
641
|
|
642 // We don't need the listener anymore.
|
|
643 data.shutdown(SocketShutdown.BOTH);
|
|
644 data.detach();
|
|
645
|
|
646 // This is the actual socket.
|
|
647 data = new_data;
|
|
648 break;
|
|
649
|
|
650 case FtpConnectionType.passive:
|
|
651 break;
|
|
652 }
|
|
653 }
|
|
654
|
|
655 /// Send a PASV and initiate a connection.
|
|
656 ///
|
|
657 /// Returns: a connected socket
|
|
658 public Socket connectPassive()
|
|
659 {
|
|
660 Address connect_to = null;
|
|
661
|
|
662 // SPSV, which is just a port number.
|
|
663 if (this.is_supported("SPSV"))
|
|
664 {
|
|
665 this.sendCommand("SPSV");
|
|
666 auto response = this.readResponse("227");
|
|
667
|
|
668 // Connecting to the same host.
|
|
669 IPv4Address remote = cast(IPv4Address) this.socket.remoteAddress();
|
|
670 assert (remote !is null);
|
|
671
|
|
672 uint address = remote.addr();
|
|
673 uint port = toInt(response.message);
|
|
674
|
|
675 connect_to = new IPv4Address(address, cast(ushort) port);
|
|
676 }
|
|
677 // Extended passive mode (IP v6, etc.)
|
|
678 else if (this.is_supported("EPSV"))
|
|
679 {
|
|
680 this.sendCommand("EPSV");
|
|
681 auto response = this.readResponse("229");
|
|
682
|
|
683 // Try to pull out the (possibly not parenthesized) address.
|
|
684 auto r = Regex.search(response.message, `\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`);
|
|
685 if (r is null)
|
|
686 throw new FTPException("CLIENT: Unable to parse address", "501");
|
|
687
|
|
688 IPv4Address remote = cast(IPv4Address) this.socket.remoteAddress();
|
|
689 assert (remote !is null);
|
|
690
|
|
691 uint address = remote.addr();
|
|
692 uint port = toInt(r.match(1));
|
|
693
|
|
694 connect_to = new IPv4Address(address, cast(ushort) port);
|
|
695 }
|
|
696 else
|
|
697 {
|
|
698 this.sendCommand("PASV");
|
|
699 auto response = this.readResponse("227");
|
|
700
|
|
701 // Try to pull out the (possibly not parenthesized) address.
|
|
702 auto r = Regex.search(response.message, `(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`);
|
|
703 if (r is null)
|
|
704 throw new FTPException("CLIENT: Unable to parse address", "501");
|
|
705
|
|
706 // Now put it into something std.socket will understand.
|
|
707 char[] address = r.match(1)~"."~r.match(2)~"."~r.match(3)~"."~r.match(4);
|
|
708 uint port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt(r.match(7)) : 0);
|
|
709
|
|
710 // Okay, we've got it!
|
|
711 connect_to = new IPv4Address(address, port);
|
|
712 }
|
|
713
|
|
714 scope (exit)
|
|
715 delete connect_to;
|
|
716
|
|
717 // This will throw an exception if it cannot connect.
|
|
718 auto sock = new Socket(AddressFamily.INET, SocketType.STREAM, ProtocolType.TCP);
|
|
719 sock.connect (connect_to);
|
|
720 return sock;
|
|
721 }
|
|
722
|
|
723 /// Change the type of data transfer.
|
|
724 ///
|
|
725 /// ASCII mode implies that line ending conversion should be made.
|
|
726 /// Only NON PRINT is supported.
|
|
727 ///
|
|
728 /// Params:
|
|
729 /// type = FtpFormat.ascii or FtpFormat.image
|
|
730 public void type(FtpFormat format)
|
|
731 {
|
|
732 if (format == FtpFormat.ascii)
|
|
733 this.sendCommand("TYPE", "A");
|
|
734 else
|
|
735 this.sendCommand("TYPE", "I");
|
|
736
|
|
737 this.readResponse("200");
|
|
738 }
|
|
739
|
|
740 /// Store a local file on the server.
|
|
741 ///
|
|
742 /// Calling this function will change the current data transfer format.
|
|
743 ///
|
|
744 /// Params:
|
|
745 /// path = the path to the remote file
|
|
746 /// local_file = the path to the local file
|
|
747 /// progress = a delegate to call with progress information
|
|
748 /// format = what format to send the data in
|
|
749
|
|
750 public void put(char[] path, char[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image)
|
|
751 in
|
|
752 {
|
|
753 assert (path.length > 0);
|
|
754 assert (local_file.length > 0);
|
|
755 }
|
|
756 body
|
|
757 {
|
|
758 // Open the file for reading...
|
|
759 auto file = new FileConduit(local_file);
|
|
760 scope (exit)
|
|
761 {
|
|
762 file.detach();
|
|
763 delete file;
|
|
764 }
|
|
765
|
|
766 // Seek to the correct place, if specified.
|
|
767 if (this.restart_pos > 0)
|
|
768 {
|
|
769 file.seek(this.restart_pos);
|
|
770 this.restart_pos = 0;
|
|
771 }
|
|
772 else
|
|
773 {
|
|
774 // Allocate space for the file, if we need to.
|
|
775 this.allocate(file.length);
|
|
776 }
|
|
777
|
|
778 // Now that it's open, we do what we always do.
|
|
779 this.put(path, file, progress, format);
|
|
780 }
|
|
781
|
|
782 /// Store data from a stream on the server.
|
|
783 ///
|
|
784 /// Calling this function will change the current data transfer format.
|
|
785 ///
|
|
786 /// Params:
|
|
787 /// path = the path to the remote file
|
|
788 /// stream = data to store, or null for a blank file
|
|
789 /// progress = a delegate to call with progress information
|
|
790 /// format = what format to send the data in
|
|
791 public void put(char[] path, InputStream stream = null, FtpProgress progress = null, FtpFormat format = FtpFormat.image)
|
|
792 in
|
|
793 {
|
|
794 assert (path.length > 0);
|
|
795 }
|
|
796 body
|
|
797 {
|
|
798 // Change to the specified format.
|
|
799 this.type(format);
|
|
800
|
|
801 // Okay server, we want to store something...
|
|
802 Socket data = this.processDataCommand("STOR", path);
|
|
803
|
|
804 // Send the stream over the socket!
|
|
805 if (stream !is null)
|
|
806 this.sendStream(data, stream, progress);
|
|
807
|
|
808 this.finishDataCommand(data);
|
|
809 }
|
|
810
|
|
811 /// Append data to a file on the server.
|
|
812 ///
|
|
813 /// Calling this function will change the current data transfer format.
|
|
814 ///
|
|
815 /// Params:
|
|
816 /// path = the path to the remote file
|
|
817 /// stream = data to append to the file
|
|
818 /// progress = a delegate to call with progress information
|
|
819 /// format = what format to send the data in
|
|
820 public void append(char[] path, InputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image)
|
|
821 in
|
|
822 {
|
|
823 assert (path.length > 0);
|
|
824 assert (stream !is null);
|
|
825 }
|
|
826 body
|
|
827 {
|
|
828 // Change to the specified format.
|
|
829 this.type(format);
|
|
830
|
|
831 // Okay server, we want to store something...
|
|
832 Socket data = this.processDataCommand("APPE", path);
|
|
833
|
|
834 // Send the stream over the socket!
|
|
835 this.sendStream(data, stream, progress);
|
|
836
|
|
837 this.finishDataCommand(data);
|
|
838 }
|
|
839
|
|
840 /// Seek to a byte offset for the next transfer.
|
|
841 ///
|
|
842 /// Params:
|
|
843 /// offset = the number of bytes to seek forward
|
|
844 public void restartSeek(size_t offset)
|
|
845 {
|
|
846 char[16] tmp;
|
|
847 this.sendCommand("REST", Integer.format (tmp, cast(long) offset));
|
|
848 this.readResponse("350");
|
|
849
|
|
850 // Set this for later use.
|
|
851 this.restart_pos = offset;
|
|
852 }
|
|
853
|
|
854 /// Allocate space for a file.
|
|
855 ///
|
|
856 /// After calling this, append() or put() should be the next command.
|
|
857 ///
|
|
858 /// Params:
|
|
859 /// bytes = the number of bytes to allocate
|
|
860 public void allocate(long bytes)
|
|
861 in
|
|
862 {
|
|
863 assert (bytes > 0);
|
|
864 }
|
|
865 body
|
|
866 {
|
|
867 char[16] tmp;
|
|
868 this.sendCommand("ALLO", Integer.format(tmp, bytes));
|
|
869 auto response = this.readResponse();
|
|
870
|
|
871 // For our purposes 200 and 202 are both fine.
|
|
872 if (response.code != "200" && response.code != "202")
|
|
873 exception (response);
|
|
874 }
|
|
875
|
|
876 /// Retrieve a remote file's contents into a local file.
|
|
877 ///
|
|
878 /// Calling this function will change the current data transfer format.
|
|
879 ///
|
|
880 /// Params:
|
|
881 /// path = the path to the remote file
|
|
882 /// local_file = the path to the local file
|
|
883 /// progress = a delegate to call with progress information
|
|
884 /// format = what format to read the data in
|
|
885 public void get(char[] path, char[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image)
|
|
886 in
|
|
887 {
|
|
888 assert (path.length > 0);
|
|
889 assert (local_file.length > 0);
|
|
890 }
|
|
891 body
|
|
892 {
|
|
893 FileConduit file = null;
|
|
894
|
|
895 // We may either create a new file...
|
|
896 if (this.restart_pos == 0)
|
|
897 file = new FileConduit (local_file, FileConduit.ReadWriteCreate);
|
|
898 // Or open an existing file, and seek to the specified position (read: not end, necessarily.)
|
|
899 else
|
|
900 {
|
|
901 file = new FileConduit (local_file, FileConduit.ReadWriteExisting);
|
|
902 file.seek(this.restart_pos);
|
|
903
|
|
904 this.restart_pos = 0;
|
|
905 }
|
|
906
|
|
907 scope (exit)
|
|
908 {
|
|
909 file.detach();
|
|
910 delete file;
|
|
911 }
|
|
912
|
|
913 // Now that it's open, we do what we always do.
|
|
914 this.get(path, file, progress, format);
|
|
915 }
|
|
916
|
|
917 /// Retrieve a remote file's contents into a local file.
|
|
918 ///
|
|
919 /// Calling this function will change the current data transfer format.
|
|
920 ///
|
|
921 /// Params:
|
|
922 /// path = the path to the remote file
|
|
923 /// stream = stream to write the data to
|
|
924 /// progress = a delegate to call with progress information
|
|
925 /// format = what format to read the data in
|
|
926 public void get(char[] path, OutputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image)
|
|
927 in
|
|
928 {
|
|
929 assert (path.length > 0);
|
|
930 assert (stream !is null);
|
|
931 }
|
|
932 body
|
|
933 {
|
|
934 // Change to the specified format.
|
|
935 this.type(format);
|
|
936
|
|
937 // Okay server, we want to get this file...
|
|
938 Socket data = this.processDataCommand("RETR", path);
|
|
939
|
|
940 // Read the stream in from the socket!
|
|
941 this.readStream(data, stream, progress);
|
|
942
|
|
943 this.finishDataCommand(data);
|
|
944 }
|
|
945
|
|
946 /// Get information about a single file.
|
|
947 ///
|
|
948 /// Return an FtpFileInfo struct about the specified path.
|
|
949 /// This may not work consistently on directories (but should.)
|
|
950 ///
|
|
951 /// Params:
|
|
952 /// path = the file or directory to get information about
|
|
953 ///
|
|
954 /// Returns: the file information
|
|
955 public FtpFileInfo getFileInfo(char[] path)
|
|
956 in
|
|
957 {
|
|
958 assert (path.length > 0);
|
|
959 }
|
|
960 body
|
|
961 {
|
|
962 // Start assuming the MLST didn't work.
|
|
963 bool mlst_success = false;
|
|
964 FtpResponse response;
|
|
965
|
|
966 // Check if MLST might be supported...
|
|
967 if (this.isSupported("MLST"))
|
|
968 {
|
|
969 this.sendCommand("MLST", path);
|
|
970 response = this.readResponse();
|
|
971
|
|
972 // If we know it was supported for sure, this is an error.
|
|
973 if (this.is_supported("MLST"))
|
|
974 exception (response);
|
|
975 // Otherwise, it probably means we need to try a LIST.
|
|
976 else
|
|
977 mlst_success = response.code == "250";
|
|
978 }
|
|
979
|
|
980 // Okay, we got the MLST response... parse it.
|
|
981 if (mlst_success)
|
|
982 {
|
|
983 char[][] lines = Text.splitLines (response.message);
|
|
984
|
|
985 // We need at least 3 lines - first and last and header/footer lines.
|
|
986 // Note that more than 3 could be returned; e.g. multiple lines about the one file.
|
|
987 if (lines.length <= 2)
|
|
988 throw new FTPException("CLIENT: Bad MLST response from server", "501");
|
|
989
|
|
990 // Return the first line's information.
|
|
991 return parseMlstLine(lines[1]);
|
|
992 }
|
|
993 else
|
|
994 {
|
|
995 // Send a list command. This may list the contents of a directory, even.
|
|
996 FtpFileInfo[] temp = this.sendListCommand(path);
|
|
997
|
|
998 // If there wasn't at least one line, the file didn't exist?
|
|
999 // We should have already handled that.
|
|
1000 if (temp.length < 1)
|
|
1001 throw new FTPException("CLIENT: Bad LIST response from server", "501");
|
|
1002
|
|
1003 // If there are multiple lines, try to return the correct one.
|
|
1004 if (temp.length != 1)
|
|
1005 foreach (FtpFileInfo info; temp)
|
|
1006 {
|
|
1007 if (info.type == FtpFileType.cdir)
|
|
1008 return info;
|
|
1009 }
|
|
1010
|
|
1011 // Okay then, the first line. Best we can do?
|
|
1012 return temp[0];
|
|
1013 }
|
|
1014 }
|
|
1015
|
|
1016 /// Get a listing of a directory's contents.
|
|
1017 ///
|
|
1018 /// Don't end path in a /. Blank means the current directory.
|
|
1019 ///
|
|
1020 /// Params:
|
|
1021 /// path = the directory to list
|
|
1022 ///
|
|
1023 /// Returns: an array of the contents
|
|
1024 public FtpFileInfo[] ls(char[] path = "") // default to current dir
|
|
1025 in
|
|
1026 {
|
|
1027 assert (path.length == 0 || path[path.length - 1] != '/');
|
|
1028 }
|
|
1029 body
|
|
1030 {
|
|
1031 FtpFileInfo[] dir;
|
|
1032
|
|
1033 // We'll try MLSD (which is so much better) first... but it may fail.
|
|
1034 bool mlsd_success = false;
|
|
1035 Socket data = null;
|
|
1036
|
|
1037 // Try it if it could/might/maybe is supported.
|
|
1038 if (this.isSupported("MLST"))
|
|
1039 {
|
|
1040 mlsd_success = true;
|
|
1041
|
|
1042 // Since this is a data command, processDataCommand handles
|
|
1043 // checking the response... just catch its Exception.
|
|
1044 try
|
|
1045 {
|
|
1046 if (path.length > 0)
|
|
1047 data = this.processDataCommand("MLSD", path);
|
|
1048 else
|
|
1049 data = this.processDataCommand("MLSD");
|
|
1050 }
|
|
1051 catch (FTPException)
|
|
1052 mlsd_success = false;
|
|
1053 }
|
|
1054
|
|
1055 // If it passed, parse away!
|
|
1056 if (mlsd_success)
|
|
1057 {
|
|
1058 auto listing = new GrowBuffer;
|
|
1059 this.readStream(data, listing);
|
|
1060 this.finishDataCommand(data);
|
|
1061
|
|
1062 // Each line is something in that directory.
|
|
1063 char[][] lines = Text.splitLines (cast(char[]) listing.slice());
|
|
1064 scope (exit)
|
|
1065 delete lines;
|
|
1066
|
|
1067 foreach (char[] line; lines)
|
|
1068 {
|
|
1069 // Parse each line exactly like MLST does.
|
|
1070 FtpFileInfo info = this.parseMlstLine(line);
|
|
1071 if (info.name.length > 0)
|
|
1072 dir ~= info;
|
|
1073 }
|
|
1074
|
|
1075 return dir;
|
|
1076 }
|
|
1077 // Fall back to LIST.
|
|
1078 else
|
|
1079 return this.sendListCommand(path);
|
|
1080 }
|
|
1081
|
|
1082 /// Send a LIST command to determine a directory's content.
|
|
1083 ///
|
|
1084 /// The format of a LIST response is not guaranteed. If available,
|
|
1085 /// MLSD should be used instead.
|
|
1086 ///
|
|
1087 /// Params:
|
|
1088 /// path = the file or directory to list
|
|
1089 ///
|
|
1090 /// Returns: an array of the contents
|
|
1091 protected FtpFileInfo[] sendListCommand(char[] path)
|
|
1092 {
|
|
1093 FtpFileInfo[] dir;
|
|
1094 Socket data = null;
|
|
1095
|
|
1096 if (path.length > 0)
|
|
1097 data = this.processDataCommand("LIST", path);
|
|
1098 else
|
|
1099 data = this.processDataCommand("LIST");
|
|
1100
|
|
1101 // Read in the stupid non-standardized response.
|
|
1102 auto listing = new GrowBuffer;
|
|
1103 this.readStream(data, listing);
|
|
1104 this.finishDataCommand(data);
|
|
1105
|
|
1106 // Split out the lines. Most of the time, it's one-to-one.
|
|
1107 char[][] lines = Text.splitLines (cast(char[]) listing.slice());
|
|
1108 scope (exit)
|
|
1109 delete lines;
|
|
1110
|
|
1111 foreach (char[] line; lines)
|
|
1112 {
|
|
1113 // If there are no spaces, or if there's only one... skip the line.
|
|
1114 // This is probably like a "total 8" line.
|
|
1115 if (Text.locate(line, ' ') == Text.locatePrior(line, ' '))
|
|
1116 continue;
|
|
1117
|
|
1118 // Now parse the line, or try to.
|
|
1119 FtpFileInfo info = this.parseListLine(line);
|
|
1120 if (info.name.length > 0)
|
|
1121 dir ~= info;
|
|
1122 }
|
|
1123
|
|
1124 return dir;
|
|
1125 }
|
|
1126
|
|
1127 /// Parse a LIST response line.
|
|
1128 ///
|
|
1129 /// The format here isn't even specified, so we have to try to detect
|
|
1130 /// commmon ones.
|
|
1131 ///
|
|
1132 /// Params:
|
|
1133 /// line = the line to parse
|
|
1134 ///
|
|
1135 /// Returns: information about the file
|
|
1136 protected FtpFileInfo parseListLine(char[] line)
|
|
1137 {
|
|
1138 FtpFileInfo info;
|
|
1139 size_t pos = 0;
|
|
1140
|
|
1141 // Convenience function to parse a word from the line.
|
|
1142 char[] parse_word()
|
|
1143 {
|
|
1144 size_t start = 0, end = 0;
|
|
1145
|
|
1146 // Skip whitespace before.
|
|
1147 while (pos < line.length && line[pos] == ' ')
|
|
1148 pos++;
|
|
1149
|
|
1150 start = pos;
|
|
1151 while (pos < line.length && line[pos] != ' ')
|
|
1152 pos++;
|
|
1153 end = pos;
|
|
1154
|
|
1155 // Skip whitespace after.
|
|
1156 while (pos < line.length && line[pos] == ' ')
|
|
1157 pos++;
|
|
1158
|
|
1159 return line[start .. end];
|
|
1160 }
|
|
1161
|
|
1162 // We have to sniff this... :/.
|
|
1163 switch (! Text.contains ("0123456789", line[0]))
|
|
1164 {
|
|
1165 // Not a number; this is UNIX format.
|
|
1166 case true:
|
|
1167 // The line must be at least 20 characters long.
|
|
1168 if (line.length < 20)
|
|
1169 return info;
|
|
1170
|
|
1171 // The first character tells us what it is.
|
|
1172 if (line[0] == 'd')
|
|
1173 info.type = FtpFileType.dir;
|
|
1174 else if (line[0] == '-')
|
|
1175 info.type = FtpFileType.file;
|
|
1176 else
|
|
1177 info.type = FtpFileType.unknown;
|
|
1178
|
|
1179 // Parse out the mode... rwxrwxrwx = 777.
|
|
1180 char[] unix_mode = "0000".dup;
|
|
1181 void read_mode(int digit)
|
|
1182 {
|
|
1183 for (pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++)
|
|
1184 {
|
|
1185 if (line[pos] == 'r')
|
|
1186 unix_mode[digit + 1] |= 4;
|
|
1187 else if (line[pos] == 'w')
|
|
1188 unix_mode[digit + 1] |= 2;
|
|
1189 else if (line[pos] == 'x')
|
|
1190 unix_mode[digit + 1] |= 1;
|
|
1191 }
|
|
1192 }
|
|
1193
|
|
1194 // This makes it easier, huh?
|
|
1195 read_mode(0);
|
|
1196 read_mode(1);
|
|
1197 read_mode(2);
|
|
1198
|
|
1199 info.facts["UNIX.mode"] = unix_mode;
|
|
1200
|
|
1201 // Links, owner, group. These are hard to translate to MLST facts.
|
|
1202 parse_word();
|
|
1203 parse_word();
|
|
1204 parse_word();
|
|
1205
|
|
1206 // Size in bytes, this one is good.
|
|
1207 info.size = toLong(parse_word());
|
|
1208
|
|
1209 // Make sure we still have enough space.
|
|
1210 if (pos + 13 >= line.length)
|
|
1211 return info;
|
|
1212
|
|
1213 // Not parsing date for now. It's too weird (last 12 months, etc.)
|
|
1214 pos += 13;
|
|
1215
|
|
1216 info.name = line[pos .. line.length];
|
|
1217 break;
|
|
1218
|
|
1219 // A number; this is DOS format.
|
|
1220 case false:
|
|
1221 // We need some data here, to parse.
|
|
1222 if (line.length < 18)
|
|
1223 return info;
|
|
1224
|
|
1225 // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P
|
|
1226 auto r = Regex.search(line, `(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`);
|
|
1227 if (r is null)
|
|
1228 return info;
|
|
1229
|
|
1230 if (Timestamp.dostime (r.match(0), info.modify) is 0)
|
|
1231 info.modify = Time.max;
|
|
1232
|
|
1233 pos = r.match(0).length;
|
|
1234 delete r;
|
|
1235
|
|
1236 // This will either be <DIR>, or a number.
|
|
1237 char[] dir_or_size = parse_word();
|
|
1238
|
|
1239 if (dir_or_size.length < 0)
|
|
1240 return info;
|
|
1241 else if (dir_or_size[0] == '<')
|
|
1242 info.type = FtpFileType.dir;
|
|
1243 else
|
|
1244 info.size = toLong(dir_or_size);
|
|
1245
|
|
1246 info.name = line[pos .. line.length];
|
|
1247 break;
|
|
1248
|
|
1249 // Something else, not supported.
|
|
1250 default:
|
|
1251 throw new FTPException("CLIENT: Unsupported LIST format", "501");
|
|
1252 }
|
|
1253
|
|
1254 // Try to fix the type?
|
|
1255 if (info.name == ".")
|
|
1256 info.type = FtpFileType.cdir;
|
|
1257 else if (info.name == "..")
|
|
1258 info.type = FtpFileType.pdir;
|
|
1259
|
|
1260 return info;
|
|
1261 }
|
|
1262
|
|
1263 /// Parse an MLST/MLSD response line.
|
|
1264 ///
|
|
1265 /// The format here is very rigid, and has facts followed by a filename.
|
|
1266 ///
|
|
1267 /// Params:
|
|
1268 /// line = the line to parse
|
|
1269 ///
|
|
1270 /// Returns: information about the file
|
|
1271 protected FtpFileInfo parseMlstLine(char[] line)
|
|
1272 {
|
|
1273 FtpFileInfo info;
|
|
1274
|
|
1275 // After this loop, filename_pos will be location of space + 1.
|
|
1276 size_t filename_pos = 0;
|
|
1277 while (filename_pos < line.length && line[filename_pos++] != ' ')
|
|
1278 continue;
|
|
1279
|
|
1280 if (filename_pos == line.length)
|
|
1281 throw new FTPException("CLIENT: Bad syntax in MLSx response", "501");
|
|
1282
|
|
1283 info.name = line[filename_pos .. line.length];
|
|
1284
|
|
1285 // Everything else is frosting on top.
|
|
1286 if (filename_pos > 1)
|
|
1287 {
|
|
1288 char[][] temp_facts = Text.delimit(line[0 .. filename_pos - 1], ";");
|
|
1289
|
|
1290 // Go through each fact and parse them into the array.
|
|
1291 foreach (char[] fact; temp_facts)
|
|
1292 {
|
|
1293 int pos = Text.locate(fact, '=');
|
|
1294 if (pos == fact.length)
|
|
1295 continue;
|
|
1296
|
|
1297 info.facts[Ascii.toLower(fact[0 .. pos])] = fact[pos + 1 .. fact.length];
|
|
1298 }
|
|
1299
|
|
1300 // Do we have a type?
|
|
1301 if ("type" in info.facts)
|
|
1302 {
|
|
1303 // Some reflection might be nice here.
|
|
1304 switch (Ascii.toLower(info.facts["type"]))
|
|
1305 {
|
|
1306 case "file":
|
|
1307 info.type = FtpFileType.file;
|
|
1308 break;
|
|
1309
|
|
1310 case "cdir":
|
|
1311 info.type = FtpFileType.cdir;
|
|
1312 break;
|
|
1313
|
|
1314 case "pdir":
|
|
1315 info.type = FtpFileType.pdir;
|
|
1316 break;
|
|
1317
|
|
1318 case "dir":
|
|
1319 info.type = FtpFileType.dir;
|
|
1320 break;
|
|
1321
|
|
1322 default:
|
|
1323 info.type = FtpFileType.other;
|
|
1324 }
|
|
1325 }
|
|
1326
|
|
1327 // Size, mime, etc...
|
|
1328 if ("size" in info.facts)
|
|
1329 info.size = toLong(info.facts["size"]);
|
|
1330 if ("media-type" in info.facts)
|
|
1331 info.mime = info.facts["media-type"];
|
|
1332
|
|
1333 // And the two dates.
|
|
1334 if ("modify" in info.facts)
|
|
1335 info.modify = this.parseTimeval(info.facts["modify"]);
|
|
1336 if ("create" in info.facts)
|
|
1337 info.create = this.parseTimeval(info.facts["create"]);
|
|
1338 }
|
|
1339
|
|
1340 return info;
|
|
1341 }
|
|
1342
|
|
1343 /// Parse a timeval from an FTP response.
|
|
1344 ///
|
|
1345 /// This is basically an ISO 8601 date, but even more rigid.
|
|
1346 ///
|
|
1347 /// Params:
|
|
1348 /// timeval = the YYYYMMDDHHMMSS date
|
|
1349 ///
|
|
1350 /// Returns: a d_time representing the same date
|
|
1351
|
|
1352 protected Time parseTimeval(char[] timeval)
|
|
1353 {
|
|
1354 if (timeval.length < 14)
|
|
1355 throw new FTPException("CLIENT: Unable to parse timeval", "501");
|
|
1356
|
|
1357 return Gregorian.generic.toTime (Integer.atoi (timeval[0..4]),
|
|
1358 Integer.atoi (timeval[4..6]),
|
|
1359 Integer.atoi (timeval[6..8]),
|
|
1360 Integer.atoi (timeval[8..10]),
|
|
1361 Integer.atoi (timeval[10..12]),
|
|
1362 Integer.atoi (timeval[12..14]));
|
|
1363 }
|
|
1364
|
|
1365 /// Get the modification time of a file.
|
|
1366 ///
|
|
1367 /// Not supported by a lot of servers.
|
|
1368 ///
|
|
1369 /// Params:
|
|
1370 /// path = the file or directory in question
|
|
1371 ///
|
|
1372 /// Returns: a d_time representing the mtime
|
|
1373 public Time filemtime(char[] path)
|
|
1374 in
|
|
1375 {
|
|
1376 assert (path.length > 0);
|
|
1377 }
|
|
1378 body
|
|
1379 {
|
|
1380 this.sendCommand("MDTM", path);
|
|
1381 auto response = this.readResponse("213");
|
|
1382
|
|
1383 // The whole response should be a timeval.
|
|
1384 return this.parseTimeval(response.message);
|
|
1385 }
|
|
1386
|
|
1387 /// Create a directory.
|
|
1388 ///
|
|
1389 /// Depending on server model, a cwd with the same path may not work.
|
|
1390 /// Use the return value instead to escape this problem.
|
|
1391 ///
|
|
1392 /// Params:
|
|
1393 /// path = the directory to create
|
|
1394 ///
|
|
1395 /// Returns: the path to the directory created
|
|
1396 public char[] mkdir(char[] path)
|
|
1397 in
|
|
1398 {
|
|
1399 assert (path.length > 0);
|
|
1400 }
|
|
1401 body
|
|
1402 {
|
|
1403 this.sendCommand("MKD", path);
|
|
1404 auto response = this.readResponse("257");
|
|
1405
|
|
1406 return this.parse257(response);
|
|
1407 }
|
|
1408
|
|
1409 /// Get supported features from the server.
|
|
1410 ///
|
|
1411 /// This may not be supported, in which case the list will remain empty.
|
|
1412 /// Otherwise, it will contain at least FEAT.
|
|
1413 public void getFeatures()
|
|
1414 {
|
|
1415 this.sendCommand("FEAT");
|
|
1416 auto response = this.readResponse();
|
|
1417
|
|
1418 // 221 means FEAT is supported, and a list follows. Otherwise we don't know...
|
|
1419 if (response.code != "211")
|
|
1420 delete this.supported_features;
|
|
1421 else
|
|
1422 {
|
|
1423 char[][] lines = Text.splitLines (response.message);
|
|
1424
|
|
1425 // There are two more lines than features, but we also have FEAT.
|
|
1426 this.supported_features = new FtpFeature[lines.length - 1];
|
|
1427 this.supported_features[0].command = "FEAT";
|
|
1428
|
|
1429 for (size_t i = 1; i < lines.length - 1; i++)
|
|
1430 {
|
|
1431 size_t pos = Text.locate(lines[i], ' ');
|
|
1432
|
|
1433 this.supported_features[i].command = lines[i][0 .. pos];
|
|
1434 if (pos < lines[i].length - 1)
|
|
1435 this.supported_features[i].params = lines[i][pos + 1 .. lines[i].length];
|
|
1436 }
|
|
1437
|
|
1438 delete lines;
|
|
1439 }
|
|
1440 }
|
|
1441
|
|
1442 /// Check if a specific feature might be supported.
|
|
1443 ///
|
|
1444 /// Example:
|
|
1445 /// ----------
|
|
1446 /// if (ftp.isSupported("SIZE"))
|
|
1447 /// size = ftp.size("example.txt");
|
|
1448 /// ----------
|
|
1449 ///
|
|
1450 /// Params:
|
|
1451 /// command = the command in question
|
|
1452 public bool isSupported(char[] command)
|
|
1453 in
|
|
1454 {
|
|
1455 assert (command.length > 0);
|
|
1456 }
|
|
1457 body
|
|
1458 {
|
|
1459 if (this.supported_features.length == 0)
|
|
1460 return true;
|
|
1461
|
|
1462 // Search through the list for the feature.
|
|
1463 foreach (FtpFeature feat; this.supported_features)
|
|
1464 {
|
|
1465 if (Ascii.icompare(feat.command, command) == 0)
|
|
1466 return true;
|
|
1467 }
|
|
1468
|
|
1469 return false;
|
|
1470 }
|
|
1471
|
|
1472 /// Check if a specific feature is known to be supported.
|
|
1473 ///
|
|
1474 /// Example:
|
|
1475 /// ----------
|
|
1476 /// if (ftp.is_supported("SIZE"))
|
|
1477 /// size = ftp.size("example.txt");
|
|
1478 /// ----------
|
|
1479 ///
|
|
1480 /// Params:
|
|
1481 /// command = the command in question
|
|
1482 public bool is_supported(char[] command)
|
|
1483 {
|
|
1484 if (this.supported_features.length == 0)
|
|
1485 return false;
|
|
1486
|
|
1487 return this.isSupported(command);
|
|
1488 }
|
|
1489
|
|
1490 /// Send a site-specific command.
|
|
1491 ///
|
|
1492 /// The command might be WHO, for example, returning a list of users online.
|
|
1493 /// These are typically heavily server-specific.
|
|
1494 ///
|
|
1495 /// Params:
|
|
1496 /// command = the command to send (after SITE)
|
|
1497 /// parameters = any additional parameters to send
|
|
1498 /// (each will be prefixed by a space)
|
|
1499 public FtpResponse siteCommand(char[] command, char[][] parameters ...)
|
|
1500 in
|
|
1501 {
|
|
1502 assert (command.length > 0);
|
|
1503 }
|
|
1504 body
|
|
1505 {
|
|
1506 // Because of the way sendCommand() works, we have to tweak this a bit.
|
|
1507 char[][] temp_params = new char[][parameters.length + 1];
|
|
1508 temp_params[0] = command;
|
|
1509 temp_params[1 .. temp_params.length][] = parameters;
|
|
1510
|
|
1511 this.sendCommand("SITE", temp_params);
|
|
1512 auto response = this.readResponse();
|
|
1513
|
|
1514 // Check to make sure it didn't fail.
|
|
1515 if (response.code[0] != '2')
|
|
1516 exception (response);
|
|
1517
|
|
1518 return response;
|
|
1519 }
|
|
1520
|
|
1521 /// Send a NOOP, typically used to keep the connection alive.
|
|
1522 public void noop()
|
|
1523 {
|
|
1524 this.sendCommand("NOOP");
|
|
1525 this.readResponse("200");
|
|
1526 }
|
|
1527
|
|
1528 /// Send the stream to the server.
|
|
1529 ///
|
|
1530 /// Params:
|
|
1531 /// data = the socket to write to
|
|
1532 /// stream = the stream to read from
|
|
1533 /// progress = a delegate to call with progress information
|
|
1534
|
|
1535 protected void sendStream(Socket data, InputStream stream, FtpProgress progress = null)
|
|
1536 in
|
|
1537 {
|
|
1538 assert (data !is null);
|
|
1539 assert (stream !is null);
|
|
1540 }
|
|
1541 body
|
|
1542 {
|
|
1543 // Set up a SocketSet so we can use select() - it's pretty efficient.
|
|
1544 SocketSet set = new SocketSet();
|
|
1545 scope (exit)
|
|
1546 delete set;
|
|
1547
|
|
1548 // At end_time, we bail.
|
|
1549 Time end_time = Clock.now + this.timeout;
|
|
1550
|
|
1551 // This is the buffer the stream data is stored in.
|
|
1552 ubyte[8 * 1024] buf;
|
|
1553 size_t buf_size = 0, buf_pos = 0;
|
|
1554 int delta = 0;
|
|
1555
|
|
1556 size_t pos = 0;
|
|
1557 bool completed = false;
|
|
1558 while (!completed && Clock.now < end_time)
|
|
1559 {
|
|
1560 set.reset();
|
|
1561 set.add(data);
|
|
1562
|
|
1563 // Can we write yet, can we write yet?
|
|
1564 int code = Socket.select(null, set, null, this.timeout);
|
|
1565 if (code == -1 || code == 0)
|
|
1566 break;
|
|
1567
|
|
1568 if (buf_size - buf_pos <= 0)
|
|
1569 {
|
|
1570 if ((buf_size = stream.read(buf)) is stream.Eof)
|
|
1571 buf_size = 0, completed = true;
|
|
1572 buf_pos = 0;
|
|
1573 }
|
|
1574
|
|
1575 // Send the chunk (or as much of it as possible!)
|
|
1576 delta = data.send(buf[buf_pos .. buf_size]);
|
|
1577 if (delta == data.ERROR)
|
|
1578 break;
|
|
1579
|
|
1580 buf_pos += delta;
|
|
1581
|
|
1582 pos += delta;
|
|
1583 if (progress !is null)
|
|
1584 progress(pos);
|
|
1585
|
|
1586 // Give it more time as long as data is going through.
|
|
1587 if (delta != 0)
|
|
1588 end_time = Clock.now + this.timeout;
|
|
1589 }
|
|
1590
|
|
1591 // Did all the data get sent?
|
|
1592 if (!completed)
|
|
1593 throw new FTPException("CLIENT: Timeout when sending data", "420");
|
|
1594 }
|
|
1595
|
|
1596 /// Reads from the server to a stream until EOF.
|
|
1597 ///
|
|
1598 /// Params:
|
|
1599 /// data = the socket to read from
|
|
1600 /// stream = the stream to write to
|
|
1601 /// progress = a delegate to call with progress information
|
|
1602 protected void readStream(Socket data, OutputStream stream, FtpProgress progress = null)
|
|
1603 in
|
|
1604 {
|
|
1605 assert (data !is null);
|
|
1606 assert (stream !is null);
|
|
1607 }
|
|
1608 body
|
|
1609 {
|
|
1610 // Set up a SocketSet so we can use select() - it's pretty efficient.
|
|
1611 SocketSet set = new SocketSet();
|
|
1612 scope (exit)
|
|
1613 delete set;
|
|
1614
|
|
1615 // At end_time, we bail.
|
|
1616 Time end_time = Clock.now + this.timeout;
|
|
1617
|
|
1618 // This is the buffer the stream data is stored in.
|
|
1619 ubyte[8 * 1024] buf;
|
|
1620 int buf_size = 0;
|
|
1621
|
|
1622 bool completed = false;
|
|
1623 size_t pos;
|
|
1624 while (Clock.now < end_time)
|
|
1625 {
|
|
1626 set.reset();
|
|
1627 set.add(data);
|
|
1628
|
|
1629 // Can we read yet, can we read yet?
|
|
1630 int code = Socket.select(set, null, null, this.timeout);
|
|
1631 if (code == -1 || code == 0)
|
|
1632 break;
|
|
1633
|
|
1634 buf_size = data.receive(buf);
|
|
1635 if (buf_size == data.ERROR)
|
|
1636 break;
|
|
1637
|
|
1638 if (buf_size == 0)
|
|
1639 {
|
|
1640 completed = true;
|
|
1641 break;
|
|
1642 }
|
|
1643
|
|
1644 stream.write(buf[0 .. buf_size]);
|
|
1645
|
|
1646 pos += buf_size;
|
|
1647 if (progress !is null)
|
|
1648 progress(pos);
|
|
1649
|
|
1650 // Give it more time as long as data is going through.
|
|
1651 end_time = Clock.now + this.timeout;
|
|
1652 }
|
|
1653
|
|
1654 // Did all the data get received?
|
|
1655 if (!completed)
|
|
1656 throw new FTPException("CLIENT: Timeout when reading data", "420");
|
|
1657 }
|
|
1658
|
|
1659 /// Parse a 257 response (which begins with a quoted path.)
|
|
1660 ///
|
|
1661 /// Params:
|
|
1662 /// response = the response to parse
|
|
1663 ///
|
|
1664 /// Returns: the path in the response
|
|
1665
|
|
1666 protected char[] parse257(FtpResponse response)
|
|
1667 {
|
|
1668 char[] path = new char[response.message.length];
|
|
1669 size_t pos = 1, len = 0;
|
|
1670
|
|
1671 // Since it should be quoted, it has to be at least 3 characters in length.
|
|
1672 if (response.message.length <= 2)
|
|
1673 exception (response);
|
|
1674
|
|
1675 assert (response.message[0] == '"');
|
|
1676
|
|
1677 // Trapse through the response...
|
|
1678 while (pos < response.message.length)
|
|
1679 {
|
|
1680 if (response.message[pos] == '"')
|
|
1681 {
|
|
1682 // An escaped quote, keep going. False alarm.
|
|
1683 if (response.message[++pos] == '"')
|
|
1684 path[len++] = response.message[pos];
|
|
1685 else
|
|
1686 break;
|
|
1687 }
|
|
1688 else
|
|
1689 path[len++] = response.message[pos];
|
|
1690
|
|
1691 pos++;
|
|
1692 }
|
|
1693
|
|
1694 // Okay, done! That wasn't too hard.
|
|
1695 path.length = len;
|
|
1696 return path;
|
|
1697 }
|
|
1698
|
|
1699 /// Send a command to the FTP server.
|
|
1700 ///
|
|
1701 /// Does not get/wait for the response.
|
|
1702 ///
|
|
1703 /// Params:
|
|
1704 /// command = the command to send
|
|
1705 /// ... = additional parameters to send (a space will be prepended to each)
|
|
1706 public void sendCommand(char[] command, char[][] parameters ...)
|
|
1707 {
|
|
1708 assert (this.socket !is null);
|
|
1709
|
|
1710
|
|
1711 char [] socketCommand = command ;
|
|
1712
|
|
1713 // Send the command, parameters, and then a CRLF.
|
|
1714
|
|
1715 foreach (char[] param; parameters)
|
|
1716 {
|
|
1717 socketCommand ~= " " ~ param;
|
|
1718
|
|
1719 }
|
|
1720
|
|
1721 socketCommand ~= "\r\n";
|
|
1722
|
|
1723 debug(FtpDebug)
|
|
1724 {
|
|
1725 Stdout.formatln("[sendCommand] Sending command '{0}'",socketCommand );
|
|
1726 }
|
|
1727 this.sendData(socketCommand);
|
|
1728 }
|
|
1729
|
|
1730 /// Read in response lines from the server, expecting a certain code.
|
|
1731 ///
|
|
1732 /// Params:
|
|
1733 /// expected_code = the code expected from the server
|
|
1734 ///
|
|
1735 /// Returns: the response from the server
|
|
1736 ///
|
|
1737 /// Throws: FTPException if code does not match
|
|
1738 public FtpResponse readResponse(char[] expected_code)
|
|
1739 {
|
|
1740 debug (FtpDebug ) { Stdout.formatln("[readResponse] Expected Response {0}",expected_code )(); }
|
|
1741 auto response = this.readResponse();
|
|
1742 debug (FtpDebug ) { Stdout.formatln("[readResponse] Actual Response {0}",response.code)(); }
|
|
1743
|
|
1744 if (response.code != expected_code)
|
|
1745 exception (response);
|
|
1746
|
|
1747
|
|
1748
|
|
1749 return response;
|
|
1750 }
|
|
1751
|
|
1752 /// Read in the response line(s) from the server.
|
|
1753 ///
|
|
1754 /// Returns: the response from the server
|
|
1755 public FtpResponse readResponse()
|
|
1756 {
|
|
1757 assert (this.socket !is null);
|
|
1758
|
|
1759 // Pick a time at which we stop reading. It can't take too long, but it could take a bit for the whole response.
|
|
1760 Time end_time = Clock.now + this.timeout * 10;
|
|
1761
|
|
1762 FtpResponse response;
|
|
1763 char[] single_line = null;
|
|
1764
|
|
1765 // Danger, Will Robinson, don't fall into an endless loop from a malicious server.
|
|
1766 while (Clock.now < end_time)
|
|
1767 {
|
|
1768 single_line = this.readLine();
|
|
1769
|
|
1770
|
|
1771 // This is the first line.
|
|
1772 if (response.message.length == 0)
|
|
1773 {
|
|
1774 // The first line must have a code and then a space or hyphen.
|
|
1775 if (single_line.length <= 4)
|
|
1776 {
|
|
1777 response.code[] = "500";
|
|
1778 break;
|
|
1779 }
|
|
1780
|
|
1781 // The code is the first three characters.
|
|
1782 response.code[] = single_line[0 .. 3];
|
|
1783 response.message = single_line[4 .. single_line.length];
|
|
1784 }
|
|
1785 // This is either an extra line, or the last line.
|
|
1786 else
|
|
1787 {
|
|
1788 response.message ~= "\n";
|
|
1789
|
|
1790 // If the line starts like "123-", that is not part of the response message.
|
|
1791 if (single_line.length > 4 && single_line[0 .. 3] == response.code)
|
|
1792 response.message ~= single_line[4 .. single_line.length];
|
|
1793 // If it starts with a space, that isn't either.
|
|
1794 else if (single_line.length > 2 && single_line[0] == ' ')
|
|
1795 response.message ~= single_line[1 .. single_line.length];
|
|
1796 else
|
|
1797 response.message ~= single_line;
|
|
1798 }
|
|
1799
|
|
1800 // We're done if the line starts like "123 ". Otherwise we're not.
|
|
1801 if (single_line.length > 4 && single_line[0 .. 3] == response.code && single_line[3] == ' ')
|
|
1802 break;
|
|
1803 }
|
|
1804
|
|
1805 return response;
|
|
1806 }
|
|
1807
|
|
1808 /// convert text to integer
|
|
1809 private int toInt (char[] s)
|
|
1810 {
|
|
1811 return cast(int) toLong (s);
|
|
1812 }
|
|
1813
|
|
1814 /// convert text to integer
|
|
1815 private long toLong (char[] s)
|
|
1816 {
|
|
1817 return Integer.parse (s);
|
|
1818 }
|
|
1819 }
|
|
1820
|
|
1821
|
|
1822
|
|
1823 /// An exception caused by an unexpected FTP response.
|
|
1824 ///
|
|
1825 /// Even after such an exception, the connection may be in a usable state.
|
|
1826 /// Use the response code to determine more information about the error.
|
|
1827 ///
|
|
1828 /// Standards: RFC 959, RFC 2228, RFC 2389, RFC 2428
|
|
1829 class FTPException: Exception
|
|
1830 {
|
|
1831 /// The three byte response code.
|
|
1832 char[3] response_code = "000";
|
|
1833
|
|
1834 /// Construct an FTPException based on a message and code.
|
|
1835 ///
|
|
1836 /// Params:
|
|
1837 /// message = the exception message
|
|
1838 /// code = the code (5xx for fatal errors)
|
|
1839 this (char[] message, char[3] code = "420")
|
|
1840 {
|
|
1841 this.response_code[] = code;
|
|
1842 super(message);
|
|
1843 }
|
|
1844
|
|
1845 /// Construct an FTPException based on a response.
|
|
1846 ///
|
|
1847 /// Params:
|
|
1848 /// r = the server response
|
|
1849 this (FtpResponse r)
|
|
1850 {
|
|
1851 this.response_code[] = r.code;
|
|
1852 super(r.message);
|
|
1853 }
|
|
1854
|
|
1855 /// A string representation of the error.
|
|
1856 char[] toString()
|
|
1857 {
|
|
1858 char[] buffer = new char[this.msg.length + 4];
|
|
1859
|
|
1860 buffer[0 .. 3] = this.response_code;
|
|
1861 buffer[3] = ' ';
|
|
1862 buffer[4 .. buffer.length] = this.msg;
|
|
1863
|
|
1864 return buffer;
|
|
1865 }
|
|
1866 }
|
|
1867
|
|
1868
|
|
1869 debug (UnitTest )
|
|
1870 {
|
|
1871 import tango.io.Stdout;
|
|
1872
|
|
1873 unittest
|
|
1874 {
|
|
1875
|
|
1876 try
|
|
1877 {
|
|
1878 /+
|
|
1879 + TODO: Fix this
|
|
1880 +
|
|
1881 auto ftp = new FTPConnection("ftp.gnu.org","anonymous","anonymous");
|
|
1882 auto dirList = ftp.ls(); // get list for current dir
|
|
1883
|
|
1884 foreach ( entry;dirList )
|
|
1885 {
|
|
1886
|
|
1887 Stdout("File :")(entry.name)("\tSize :")(entry.size).newline;
|
|
1888
|
|
1889 }
|
|
1890
|
|
1891 ftp.cd("gnu/windows/emacs");
|
|
1892
|
|
1893
|
|
1894 dirList = ftp.ls();
|
|
1895
|
|
1896 foreach ( entry;dirList )
|
|
1897 {
|
|
1898
|
|
1899 Stdout("File :")(entry.name)("\tSize :")(entry.size).newline;
|
|
1900
|
|
1901 }
|
|
1902
|
|
1903
|
|
1904 size_t size = ftp.size("emacs-21.3-barebin-i386.tar.gz");
|
|
1905
|
|
1906 void progress( size_t pos )
|
|
1907 {
|
|
1908
|
|
1909 Stdout.formatln("Byte {0} of {1}",pos,size);
|
|
1910
|
|
1911 }
|
|
1912
|
|
1913
|
|
1914 ftp.get("emacs-21.3-barebin-i386.tar.gz","emacs.tgz", &progress);
|
|
1915 +/
|
|
1916 }
|
|
1917 catch( Object o )
|
|
1918 {
|
|
1919 assert( false );
|
|
1920 }
|
|
1921 }
|
|
1922 }
|