| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779 | /** * Author: Lester L. Martin II * UWB, bobef * Copyright: (c) Lester L. Martin II * UWB, bobef * Based upon prior FtpClient.d * License: BSD style: $(LICENSE) * Initial release: August 8, 2008 */ module tango.net.ftp.FtpClient; private { import tango.net.ftp.Telnet; import tango.net.device.Berkeley; import tango.text.Util; import tango.time.Clock; import tango.text.Regex: Regex; import tango.time.chrono.Gregorian; import tango.core.Array; import tango.net.device.Socket; import tango.io.device.Conduit; import tango.io.device.Array; import tango.io.device.File; import Text = tango.text.Util; import Ascii = tango.text.Ascii; import Integer = tango.text.convert.Integer; import Timestamp = tango.text.convert.TimeStamp; } /****************************************************************************** An FTP progress delegate. You may need to add the restart position to this, and use SIZE to determine percentage completion. This only represents the number of bytes transferred. Params: pos = the current offset into the stream ******************************************************************************/ alias void delegate(in size_t pos) FtpProgress; /****************************************************************************** The format of data transfer. ******************************************************************************/ enum FtpFormat { /********************************************************************** Indicates ASCII NON PRINT format (line ending conversion to CRLF.) **********************************************************************/ ascii, /********************************************************************** Indicates IMAGE format (8 bit binary octets.) **********************************************************************/ image, } /****************************************************************************** A FtpAddress structure that contains all that is needed to access a FTPConnection; Contributed by Bobef Since: 0.99.8 ******************************************************************************/ struct FtpAddress { static FtpAddress* opCall(const(char)[] str) { if(str.length == 0) return null; try { auto ret = new FtpAddress; //remove ftp:// auto i = locatePattern(str, "ftp://"); if(i == 0) str = str[6 .. $]; //check for username and/or password user[:pass]@ i = locatePrior(str, '@'); if(i != str.length) { const(char)[] up = str[0 .. i]; str = str[i + 1 .. $]; i = locate(up, ':'); if(i != up.length) { ret.user = up[0 .. i]; ret.pass = up[i + 1 .. $]; } else ret.user = up; } //check for port i = locatePrior(str, ':'); if(i != str.length) { ret.port = cast(uint) Integer.toLong(str[i + 1 .. $]); str = str[0 .. i]; } //check any directories after the adress i = locate(str, '/'); if(i != str.length) ret.directory = str[i + 1 .. $]; //the rest should be the address ret.address = str[0 .. i]; if(ret.address.length == 0) return null; return ret; } catch(Throwable o) { return null; } } const(char)[] address; const(char)[] directory; const(char)[] user = "anonymous"; const(char)[] pass = "anonymous@anonymous"; uint port = 21; } /****************************************************************************** A server response, consisting of a code and a potentially multi-line message. ******************************************************************************/ struct FtpResponse { /********************************************************************** The response code. The digits in the response code can be used to determine status programatically. First Digit (status): 1xx = a positive, but preliminary, reply 2xx = a positive reply indicating completion 3xx = a positive reply indicating incomplete status 4xx = a temporary negative reply 5xx = a permanent negative reply Second Digit (subject): x0x = condition based on syntax x1x = informational x2x = connection x3x = authentication/process x5x = file system **********************************************************************/ char[3] code = "000"; /********************************************************************* The message from the server. With some responses, the message may contain parseable information. For example, this is true of the 257 response. **********************************************************************/ const(char)[] message = null; } /****************************************************************************** Active or passive connection mode. ******************************************************************************/ enum FtpConnectionType { /********************************************************************** Active - server connects to client on open port. **********************************************************************/ active, /********************************************************************** Passive - server listens for a connection from the client. **********************************************************************/ passive, } /****************************************************************************** Detail about the data connection. This is used to properly send PORT and PASV commands. ******************************************************************************/ struct FtpConnectionDetail { /********************************************************************** The type to be used. **********************************************************************/ FtpConnectionType type = FtpConnectionType.passive; /********************************************************************** The address to give the server. **********************************************************************/ Address address = null; /********************************************************************** The address to actually listen on. **********************************************************************/ Address listen = null; } /****************************************************************************** A supported feature of an FTP server. ******************************************************************************/ struct FtpFeature { /********************************************************************** The command which is supported, e.g. SIZE. **********************************************************************/ const(char)[] command = null; /********************************************************************** Parameters for this command; e.g. facts for MLST. **********************************************************************/ const(char)[] params = null; } /****************************************************************************** The type of a file in an FTP listing. ******************************************************************************/ enum FtpFileType { /********************************************************************** An unknown file or type (no type fact.) **********************************************************************/ unknown, /********************************************************************** A regular file, or similar. **********************************************************************/ file, /********************************************************************** The current directory (e.g. ., but not necessarily.) **********************************************************************/ cdir, /********************************************************************** A parent directory (usually "..".) **********************************************************************/ pdir, /********************************************************************** Any other type of directory. **********************************************************************/ dir, /********************************************************************** Another type of file. Consult the "type" fact. **********************************************************************/ other, } /****************************************************************************** Information about a file in an FTP listing. ******************************************************************************/ struct FtpFileInfo { /********************************************************************** The filename. **********************************************************************/ const(char)[] name = null; /********************************************************************** Its type. **********************************************************************/ FtpFileType type = FtpFileType.unknown; /********************************************************************** Size in bytes (8 bit octets), or ulong.max if not available. Since: 0.99.8 **********************************************************************/ ulong size = ulong.max; /********************************************************************** Modification time, if available. **********************************************************************/ Time modify = Time.max; /********************************************************************** Creation time, if available (not often.) **********************************************************************/ Time create = Time.max; /********************************************************************** The file's mime type, if known. **********************************************************************/ const(char)[] mime = null; /*********************************************************************** An associative array of all facts returned by the server, lowercased. ***********************************************************************/ const(char)[][const(char)[]] facts; } /******************************************************************************* Changed location Since: 0.99.8 Documentation Pending *******************************************************************************/ class FtpException: Exception { char[3] responseCode_ = "000"; /*********************************************************************** Construct an FtpException based on a message and code. Params: message = the exception message code = the code (5xx for fatal errors) ***********************************************************************/ this(string message, char[3] code = "420") { this.responseCode_[] = code; super(message); } /*********************************************************************** Construct an FtpException based on a response. Params: r = the server response ***********************************************************************/ this(FtpResponse r) { this.responseCode_[] = r.code; super(r.message.idup); } /*********************************************************************** A string representation of the error. ***********************************************************************/ override string toString() { char[] buffer = new char[this.msg.length + 4]; buffer[0 .. 3] = this.responseCode_; buffer[3] = ' '; buffer[4 .. buffer.length] = this.msg; return buffer.idup; } } /******************************************************************************* Seriously changed Since: 0.99.8 Documentation pending *******************************************************************************/ class FTPConnection: Telnet { FtpFeature[] supportedFeatures_ = null; FtpConnectionDetail inf_; size_t restartPos_ = 0; const(char)[] currFile_ = ""; Socket dataSocket_; TimeSpan timeout_ = TimeSpan.fromMillis(5000); /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ @property public TimeSpan timeout() { return timeout_; } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ @property public void timeout(TimeSpan t) { timeout_ = t; } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ public TimeSpan shutdownTime() { return timeout_ + timeout_; } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ public FtpFeature[] supportedFeatures() { if(supportedFeatures_ !is null) { return supportedFeatures_; } getFeatures(); return supportedFeatures_; } /*********************************************************************** Changed Since: 0.99.8 ***********************************************************************/ override void exception(string message) { throw new FtpException(message); } /*********************************************************************** Changed Since: 0.99.8 ***********************************************************************/ void exception(FtpResponse fr) { exception(fr.message.idup); } public this() { } public this(const(char)[] hostname, const(char)[] username = "anonymous", const(char)[] password = "anonymous@anonymous", uint port = 21) { this.connect(hostname, username, password, port); } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ public this(FtpAddress fad) { connect(fad); } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ public void connect(FtpAddress fad) { this.connect(fad.address, fad.user, fad.pass, fad.port); } /************************************************************************ Changed Since: 0.99.8 ************************************************************************/ public void connect(const(char)[] hostname, const(char)[] username = "anonymous", const(char)[] password = "anonymous@anonymous", uint port = 21) in { // We definitely need a hostname and port. assert(hostname.length > 0); assert(port > 0); } body { if(socket_ !is null) { socket_.close(); } this.findAvailableServer(hostname, port); scope(failure) { close(); } readResponse("220"); if(username.length == 0) { return; } sendCommand("USER", username); FtpResponse response = readResponse(); if(response.code == "331") { sendCommand("PASS", password); response = readResponse(); } if(response.code != "230" && response.code != "202") { exception(response); } } public void close() { //make sure no open data connection and if open data connection then kill if(dataSocket_ !is null) this.finishDataCommand(dataSocket_); if(socket_ !is null) { try { sendCommand("QUIT"); readResponse("221"); } catch(FtpException) { } socket_.close(); delete supportedFeatures_; delete socket_; } } public void setPassive() { inf_.type = FtpConnectionType.passive; delete inf_.address; delete inf_.listen; } public void setActive(const(char)[] ip, ushort port, const(char)[] listen_ip = null, ushort listen_port = 0) in { assert(ip.length > 0); assert(port > 0); } body { inf_.type = FtpConnectionType.active; inf_.address = new IPv4Address(ip, port); // A local-side port? if(listen_port == 0) listen_port = port; // Any specific IP to listen on? if(listen_ip == null) inf_.listen = new IPv4Address(IPv4Address.ADDR_ANY, listen_port); else inf_.listen = new IPv4Address(listen_ip, listen_port); } public void cd(const(char)[] dir) in { assert(dir.length > 0); } body { sendCommand("CWD", dir); readResponse("250"); } public void cdup() { sendCommand("CDUP"); FtpResponse fr = readResponse(); if(fr.code == "200" || fr.code == "250") return; else exception(fr); } public const(char)[] cwd() { sendCommand("PWD"); auto response = readResponse("257"); return parse257(response); } public void chmod(const(char)[] path, int mode) in { assert(path.length > 0); assert(mode >= 0 && (mode >> 16) == 0); } body { char[3] tmp = "000"; // Convert our octal parameter to a string. Integer.format(tmp, cast(long) mode, "o"); sendCommand("SITE CHMOD", tmp, path); readResponse("200"); } public void del(const(char)[] path) in { assert(path.length > 0); } body { sendCommand("DELE", path); auto response = readResponse("250"); //Try it as a directory, then...? if(response.code != "250") rm(path); } public void rm(const(char)[] path) in { assert(path.length > 0); } body { sendCommand("RMD", path); readResponse("250"); } public void rename(const(char)[] old_path, const(char)[] new_path) in { assert(old_path.length > 0); assert(new_path.length > 0); } body { // Rename from... rename to. Pretty simple. sendCommand("RNFR", old_path); readResponse("350"); sendCommand("RNTO", new_path); readResponse("250"); } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ int exist(const(char)[] file) { try { auto fi = getFileInfo(file); if(fi.type == FtpFileType.file) { return 1; } else if(fi.type == FtpFileType.dir || fi.type == FtpFileType.cdir || fi.type == FtpFileType.pdir) { return 2; } } catch(FtpException o) { if(o.responseCode_ != "501") { return 0; } } return 0; } public size_t size(const(char)[] path, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); } body { type(format); sendCommand("SIZE", path); auto response = this.readResponse("213"); // Only try to parse the numeric bytes of the response. size_t end_pos = 0; while(end_pos < response.message.length) { if(response.message[end_pos] < '0' || response.message[end_pos] > '9') break; end_pos++; } return cast(int) Integer.parse((response.message[0 .. end_pos])); } public void type(FtpFormat format) { if(format == FtpFormat.ascii) sendCommand("TYPE", "A"); else sendCommand("TYPE", "I"); readResponse("200"); } /*********************************************************************** Added Since: 0.99.8 ***********************************************************************/ Time modified(const(char)[] file) in { assert(file.length > 0); } body { this.sendCommand("MDTM", file); auto response = this.readResponse("213"); // The whole response should be a timeval. return this.parseTimeval(response.message); } protected Time parseTimeval(const(char)[] timeval) { if(timeval.length < 14) throw new FtpException("CLIENT: Unable to parse timeval", "501"); return Gregorian.generic.toTime( Integer.atoi(timeval[0 .. 4]), Integer.atoi(timeval[4 .. 6]), Integer.atoi(timeval[6 .. 8]), Integer.atoi(timeval[8 .. 10]), Integer.atoi(timeval[10 .. 12]), Integer.atoi(timeval[12 .. 14])); } public void noop() { this.sendCommand("NOOP"); this.readResponse("200"); } public const(char)[] mkdir(const(char)[] path) in { assert(path.length > 0); } body { this.sendCommand("MKD", path); auto response = this.readResponse("257"); return this.parse257(response); } public void getFeatures() { this.sendCommand("FEAT"); auto response = this.readResponse(); // 221 means FEAT is supported, and a list follows. Otherwise we don't know... if(response.code != "211") delete supportedFeatures_; else { const(char)[][] lines = Text.splitLines(response.message); // There are two more lines than features, but we also have FEAT. supportedFeatures_ = new FtpFeature[lines.length - 1]; supportedFeatures_[0].command = "FEAT"; for(size_t i = 1; i < lines.length - 1; i++) { size_t pos = Text.locate(lines[i], ' '); supportedFeatures_[i].command = lines[i][0 .. pos]; if(pos < lines[i].length - 1) supportedFeatures_[i].params = lines[i][pos + 1 .. lines[i].length]; } delete lines; } } public void sendCommand(const(char)[] command, const(char)[][] parameters...) { const(char)[] socketCommand = command; // Send the command, parameters, and then a CRLF. foreach(const(char)[] param; parameters) { socketCommand ~= " " ~ param; } socketCommand ~= "\r\n"; debug(FtpDebug) { Stdout.formatln("[sendCommand] Sending command '{0}'", socketCommand); } sendData(socketCommand); } public FtpResponse readResponse(const(char)[] expected_code) { debug(FtpDebug) { Stdout.formatln("[readResponse] Expected Response {0}", expected_code)(); } auto response = readResponse(); debug(FtpDebug) { Stdout.formatln("[readResponse] Actual Response {0}", response.code)(); } if(response.code != expected_code) exception(response); return response; } public FtpResponse readResponse() { assert(this.socket_ !is null); // Pick a time at which we stop reading. It can't take too long, but it could take a bit for the whole response. Time end_time = Clock.now + TimeSpan.fromMillis(2500) * 10; FtpResponse response; const(char)[] single_line = null; // Danger, Will Robinson, don't fall into an endless loop from a malicious server. while(Clock.now < end_time) { single_line = this.readLine(); // This is the first line. if(response.message.length == 0) { // The first line must have a code and then a space or hyphen. // #1 // Response might be exactly 4 chars e.g. '230-' // (see ftp-stud.fht-esslingen.de or ftp.sunfreeware.com) if(single_line.length < 4) { response.code[] = "500"; break; } // The code is the first three characters. response.code[] = single_line[0 .. 3]; response.message = single_line[4 .. single_line.length]; } // This is either an extra line, or the last line. else { response.message ~= "\n"; // If the line starts like "123-", that is not part of the response message. if(single_line.length > 4 && single_line[0 .. 3] == response.code) response.message ~= single_line[4 .. single_line.length]; // If it starts with a space, that isn't either. else if(single_line.length > 2 && single_line[0] == ' ') response.message ~= single_line[1 .. single_line.length]; else response.message ~= single_line; } // We're done if the line starts like "123 ". Otherwise we're not. // #1 // Response might be exactly 4 chars e.g. '220 ' // (see ftp.knoppix.nl) if(single_line.length >= 4 && single_line[0 .. 3] == response.code && single_line[3] == ' ') break; } return response; } protected const(char)[] parse257(FtpResponse response) { char[] path = new char[response.message.length]; size_t pos = 1, len = 0; // Since it should be quoted, it has to be at least 3 characters in length. if(response.message.length <= 2) exception(response); //assert (response.message[0] == '"'); // Trapse through the response... while(pos < response.message.length) { if(response.message[pos] == '"') { // #2 // Is it the last character? if(pos + 1 == response.message.length) // then we are done break; // An escaped quote, keep going. False alarm. if(response.message[++pos] == '"') path[len++] = response.message[pos]; else break; } else path[len++] = response.message[pos]; pos++; } // Okay, done! That wasn't too hard. path.length = len; return path; } /******************************************************************************* Get a data socket from the server. This sends PASV/PORT as necessary. Returns: the data socket or a listener Changed Since: 0.99.8 *******************************************************************************/ protected Socket getDataSocket() { //make sure no open data connection and if open data connection then kill if(dataSocket_ !is null) this.finishDataCommand(dataSocket_); // What type are we using? switch(this.inf_.type) { default: exception("unknown connection type"); assert(0); // Passive is complicated. Handle it in another member. case FtpConnectionType.passive: return this.connectPassive(); // Active is simpler, but not as fool-proof. case FtpConnectionType.active: IPv4Address data_addr = cast(IPv4Address) this.inf_.address; // Start listening. Socket listener = new Socket; listener.bind(this.inf_.listen); listener.socket.listen(32); // Use EPRT if we know it's supported. if(this.is_supported("EPRT")) { char[64] tmp = void; this.sendCommand("EPRT", Text.layout(tmp, "|1|%0|%1|", data_addr.toAddrString, data_addr.toPortString)); // this.sendCommand("EPRT", format("|1|%s|%s|", data_addr.toAddrString(), data_addr.toPortString())); this.readResponse("200"); } else { int h1, h2, h3, h4, p1, p2; h1 = (data_addr.addr() >> 24) % 256; h2 = (data_addr.addr() >> 16) % 256; h3 = (data_addr.addr() >> 8_) % 256; h4 = (data_addr.addr() >> 0_) % 256; p1 = (data_addr.port() >> 8_) % 256; p2 = (data_addr.port() >> 0_) % 256; // low overhead method to format a numerical string char[64] tmp = void; char[20] foo = void; auto str = Text.layout(tmp, "%0,%1,%2,%3,%4,%5", Integer.format(foo[0 .. 3], h1), Integer.format(foo[3 .. 6], h2), Integer.format(foo[6 .. 9], h3), Integer.format(foo[9 .. 12], h4), Integer.format(foo[12 .. 15], p1), Integer.format(foo[15 .. 18], p2)); // This formatting is weird. // this.sendCommand("PORT", format("%d,%d,%d,%d,%d,%d", h1, h2, h3, h4, p1, p2)); this.sendCommand("PORT", str); this.readResponse("200"); } return listener; } } /******************************************************************************* Send a PASV and initiate a connection. Returns: a connected socket Changed Since: 0.99.8 *******************************************************************************/ version(TangoDoc) { public Socket connectPassive(); } else { public Socket connectPassive() { Address connect_to = null; // SPSV, which is just a port number. if(this.is_supported("SPSV")) { this.sendCommand("SPSV"); auto response = this.readResponse("227"); // Connecting to the same host. IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); assert(remote !is null); uint address = remote.addr(); uint port = cast(int) Integer.parse(((response.message))); connect_to = new IPv4Address(address, cast(ushort) port); } // Extended passive mode (IP v6, etc.) else if(this.is_supported("EPSV")) { this.sendCommand("EPSV"); auto response = this.readResponse("229"); // Try to pull out the (possibly not parenthesized) address. auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); if(!r.test(response.message[0 .. find(response.message, '\n')])) throw new FtpException("CLIENT: Unable to parse address", "501"); IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); assert(remote !is null); uint address = remote.addr(); uint port = cast(int) Integer.parse(((r.match(1)))); connect_to = new IPv4Address(address, cast(ushort) port); } else { this.sendCommand("PASV"); auto response = this.readResponse("227"); // Try to pull out the (possibly not parenthesized) address. auto r = Regex(`(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); if(!r.test(response.message[0 .. find(response.message, '\n')])) throw new FtpException("CLIENT: Unable to parse address", "501"); // Now put it into something std.socket will understand. const(char)[] address = r.match(1) ~ "." ~ r.match(2) ~ "." ~ r.match(3) ~ "." ~ r.match(4); uint port = (((cast(int) Integer.parse(r.match(5))) << 8) + (r.match(7). length > 0 ? cast(int) Integer.parse(r.match(7)) : 0)); // Okay, we've got it! connect_to = new IPv4Address(address, cast(ushort)port); } scope(exit) delete connect_to; // This will throw an exception if it cannot connect. auto sock = new Socket; sock.connect(connect_to); return sock; } } /* Socket sock = new Socket(); sock.connect(connect_to); return sock; */ public bool isSupported(const(char)[] command) in { assert(command.length > 0); } body { if(this.supportedFeatures_.length == 0) return true; // Search through the list for the feature. foreach(FtpFeature feat; this.supportedFeatures_) { if(Ascii.icompare(feat.command, command) == 0) return true; } return false; } public bool is_supported(const(char)[] command) { if(this.supportedFeatures_.length == 0) return false; return this.isSupported(command); } /******************************************************************************* Prepare a data socket for use. This modifies the socket in some cases. Params: data = the data listener socket Changed Since: 0.99.8 ********************************************************************************/ protected void prepareDataSocket(ref Socket data) { switch(this.inf_.type) { default: exception("unknown connection type"); assert(0); case FtpConnectionType.active: Berkeley new_data; scope set = new SocketSet; // At end_time, we bail. Time end_time = Clock.now + this.timeout; while(Clock.now < end_time) { set.reset(); set.add(data.socket); // Can we accept yet? int code = set.select(set, null, null, timeout.micros); if(code == -1 || code == 0) break; data.socket.accept(new_data); break; } if(new_data.sock is new_data.sock.init) throw new FtpException("CLIENT: No connection from server", "420"); // We don't need the listener anymore. data.shutdown().detach(); // This is the actual socket. data.socket.sock = new_data.sock; break; case FtpConnectionType.passive: break; } } /***************************************************************************** Changed Since: 0.99.8 *****************************************************************************/ public void finishDataCommand(Socket data) { // Close the socket. This tells the server we're done (EOF.) data.close(); data.detach(); // We shouldn't get a 250 in STREAM mode. FtpResponse r = readResponse(); if(!(r.code == "226" || r.code == "420")) exception("Bad finish"); } /***************************************************************************** Changed Since: 0.99.8 *****************************************************************************/ public Socket processDataCommand(const(char)[] command, const(char)[][] parameters...) { // Create a connection. Socket data = this.getDataSocket(); scope(failure) { // Close the socket, whether we were listening or not. data.close(); } // Tell the server about it. this.sendCommand(command, parameters); // We should always get a 150/125 response. auto response = this.readResponse(); if(response.code != "150" && response.code != "125") exception(response); // We might need to do this for active connections. this.prepareDataSocket(data); return data; } public FtpFileInfo[] ls(const(char)[] path = "") // default to current dir in { assert(path.length == 0 || path[path.length - 1] != '/'); } body { FtpFileInfo[] dir; // We'll try MLSD (which is so much better) first... but it may fail. bool mlsd_success = false; Socket data = null; // Try it if it could/might/maybe is supported. if(this.isSupported("MLST")) { mlsd_success = true; // Since this is a data command, processDataCommand handles // checking the response... just catch its Exception. try { if(path.length > 0) data = this.processDataCommand("MLSD", path); else data = this.processDataCommand("MLSD"); } catch(FtpException) mlsd_success = false; } // If it passed, parse away! if(mlsd_success) { auto listing = new Array(256, 65536); this.readStream(data, listing); this.finishDataCommand(data); // Each line is something in that directory. const(char)[][] lines = Text.splitLines(cast(const(char)[]) listing.slice()); scope(exit) delete lines; foreach(const(char)[] line; lines) { if(line.length == 0) continue; // Parse each line exactly like MLST does. try { FtpFileInfo info = this.parseMlstLine(line); if(info.name.length > 0) dir ~= info; } catch(FtpException) { return this.sendListCommand(path); } } return dir; } // Fall back to LIST. else return this.sendListCommand(path); } /***************************************************************************** Changed Since: 0.99.8 *****************************************************************************/ protected void readStream(Socket data, OutputStream stream, FtpProgress progress = null) in { assert(data !is null); assert(stream !is null); } body { // Set up a SocketSet so we can use select() - it's pretty efficient. scope set = new SocketSet; // At end_time, we bail. Time end_time = Clock.now + this.timeout; // This is the buffer the stream data is stored in. ubyte[8 * 1024] buf; int buf_size = 0; bool completed = false; size_t pos; while(Clock.now < end_time) { set.reset(); set.add(data.socket); // Can we read yet, can we read yet? int code = set.select(set, null, null, timeout.micros); if(code == -1 || code == 0) break; buf_size = data.socket.receive(buf); if(buf_size == data.socket.ERROR) break; if(buf_size == 0) { completed = true; break; } stream.write(buf[0 .. buf_size]); pos += buf_size; if(progress !is null) progress(pos); // Give it more time as long as data is going through. end_time = Clock.now + this.timeout; } // Did all the data get received? if(!completed) throw new FtpException("CLIENT: Timeout when reading data", "420"); } /***************************************************************************** Changed Since: 0.99.8 *****************************************************************************/ protected void sendStream(Socket data, InputStream stream, FtpProgress progress = null) in { assert(data !is null); assert(stream !is null); } body { // Set up a SocketSet so we can use select() - it's pretty efficient. scope set = new SocketSet; // At end_time, we bail. Time end_time = Clock.now + this.timeout; // This is the buffer the stream data is stored in. ubyte[8 * 1024] buf; size_t buf_size = 0, buf_pos = 0; int delta = 0; size_t pos = 0; bool completed = false; while(!completed && Clock.now < end_time) { set.reset(); set.add(data.socket); // Can we write yet, can we write yet? int code = set.select(null, set, null, timeout.micros); if(code == -1 || code == 0) break; if(buf_size - buf_pos <= 0) { if((buf_size = stream.read(buf)) is stream.Eof) buf_size = 0 , completed = true; buf_pos = 0; } // Send the chunk (or as much of it as possible!) delta = data.socket.send(buf[buf_pos .. buf_size]); if(delta == data.socket.ERROR) break; buf_pos += delta; pos += delta; if(progress !is null) progress(pos); // Give it more time as long as data is going through. if(delta != 0) end_time = Clock.now + this.timeout; } // Did all the data get sent? if(!completed) throw new FtpException("CLIENT: Timeout when sending data", "420"); } protected FtpFileInfo[] sendListCommand(const(char)[] path) { FtpFileInfo[] dir; Socket data = null; if(path.length > 0) data = this.processDataCommand("LIST", path); else data = this.processDataCommand("LIST"); // Read in the stupid non-standardized response. auto listing = new Array(256, 65536); this.readStream(data, listing); this.finishDataCommand(data); // Split out the lines. Most of the time, it's one-to-one. const(char)[][] lines = Text.splitLines(cast(const(char)[]) listing.slice()); scope(exit) delete lines; foreach(const(char)[] line; lines) { if(line.length == 0) continue; // If there are no spaces, or if there's only one... skip the line. // This is probably like a "total 8" line. if(Text.locate(line, ' ') == Text.locatePrior(line, cast(const(char))' ')) continue; // Now parse the line, or try to. FtpFileInfo info = this.parseListLine(line); if(info.name.length > 0) dir ~= info; } return dir; } protected FtpFileInfo parseListLine(const(char)[] line) { FtpFileInfo info; size_t pos = 0; // Convenience function to parse a word from the line. const(char)[] parse_word() { size_t start = 0, end = 0; // Skip whitespace before. while(pos < line.length && line[pos] == ' ') pos++; start = pos; while(pos < line.length && line[pos] != ' ') pos++; end = pos; // Skip whitespace after. while(pos < line.length && line[pos] == ' ') pos++; return line[start .. end]; } // We have to sniff this... :/. switch(!Text.contains("0123456789", line[0])) { // Not a number; this is UNIX format. case true: // The line must be at least 20 characters long. if(line.length < 20) return info; // The first character tells us what it is. if(line[0] == 'd') info.type = FtpFileType.dir; // #3 // Might be a link entry - additional test down below else if(line[0] == 'l') info.type = FtpFileType.other; else if(line[0] == '-') info.type = FtpFileType.file; else info.type = FtpFileType.unknown; // Parse out the mode... rwxrwxrwx = 777. char[] unix_mode = "0000".dup; void read_mode(int digit) { for(pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) { if(line[pos] == 'r') unix_mode[digit + 1] |= 4; else if(line[pos] == 'w') unix_mode[digit + 1] |= 2; else if(line[pos] == 'x') unix_mode[digit + 1] |= 1; } } // This makes it easier, huh? read_mode(0); read_mode(1); read_mode(2); info.facts["UNIX.mode"] = unix_mode; // #4 // Not only parse lines like // drwxrwxr-x 2 10490 100 4096 May 20 2005 Acrobat // lrwxrwxrwx 1 root other 7 Sep 21 2007 Broker.link -> Acrobat // -rwxrwxr-x 1 filelib 100 468 Nov 1 1999 Web_Users_Click_Here.html // but also parse lines like // d--x--x--x 2 staff 512 Sep 24 2000 dev // (see ftp.sunfreeware.com) // Links, owner. These are hard to translate to MLST facts. parse_word(); parse_word(); // Group or size in bytes const(char)[] group_or_size = parse_word(); size_t oldpos = pos; // Size in bytes or month const(char)[] size_or_month = parse_word(); if(!Text.contains("0123456789", size_or_month[0])) { // Oops, no size here - go back to previous column pos = oldpos; info.size = cast(ulong) Integer.parse(group_or_size); } else info.size = cast(ulong) Integer.parse(size_or_month); // Make sure we still have enough space. if(pos + 13 >= line.length) return info; // Not parsing date for now. It's too weird (last 12 months, etc.) pos += 13; info.name = line[pos .. line.length]; // #3 // Might be a link entry - additional test here if(info.type == FtpFileType.other) { // Is name like 'name -> /some/other/path'? size_t pos2 = Text.locatePattern(info.name, cast(const(char)[])" -> "); if(pos2 != info.name.length) { // It is a link - split into target and name info.facts["target"] = info.name[pos2 + 4 .. info.name.length]; info.name = info.name[0 .. pos2]; info.facts["type"] = "link"; } } break; // A number; this is DOS format. case false: // We need some data here, to parse. if(line.length < 18) return info; // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); // #5 // wrong test if(!r.test(line)) return info; if(Timestamp.dostime(r.match(0), info.modify) is 0) info.modify = Time.max; pos = r.match(0).length; delete r; // This will either be <DIR>, or a number. const(char)[] dir_or_size = parse_word(); if(dir_or_size.length < 0) return info; else if(dir_or_size[0] == '<') info.type = FtpFileType.dir; else { // #5 // It is a file info.size = cast(ulong) Integer.parse((dir_or_size)); info.type = FtpFileType.file; } info.name = line[pos .. line.length]; break; // Something else, not supported. default: throw new FtpException("CLIENT: Unsupported LIST format", "501"); } // Try to fix the type? if(info.name == ".") info.type = FtpFileType.cdir; else if(info.name == "..") info.type = FtpFileType.pdir; return info; } protected FtpFileInfo parseMlstLine(const(char)[] line) { FtpFileInfo info; // After this loop, filename_pos will be location of space + 1. size_t filename_pos = 0; while(filename_pos < line.length && line[filename_pos++] != ' ') continue; if(filename_pos == line.length) throw new FtpException("CLIENT: Bad syntax in MLSx response", "501"); /*{ info.name = ""; return info; }*/ info.name = line[filename_pos .. line.length]; // Everything else is frosting on top. if(filename_pos > 1) { const(char)[][] temp_facts = Text.delimit(line[0 .. filename_pos - 1], cast(const(char)[])";"); // Go through each fact and parse them into the array. foreach(const(char)[] fact; temp_facts) { size_t pos = Text.locate(fact, '='); if(pos == fact.length) continue; info.facts[cast(immutable(char)[])Ascii.toLower(fact[0 .. pos].dup)] = fact[pos + 1 .. fact.length]; } // Do we have a type? if("type" in info.facts) { // Some reflection might be nice here. switch(Ascii.toLower(info.facts["type"].dup)) { case "file": info.type = FtpFileType.file; break; case "cdir": info.type = FtpFileType.cdir; break; case "pdir": info.type = FtpFileType.pdir; break; case "dir": info.type = FtpFileType.dir; break; default: info.type = FtpFileType.other; } } // Size, mime, etc... if("size" in info.facts) info.size = cast(ulong) Integer.parse((info.facts["size"])); if("media-type" in info.facts) info.mime = info.facts["media-type"]; // And the two dates. if("modify" in info.facts) info.modify = this.parseTimeval(info.facts["modify"]); if("create" in info.facts) info.create = this.parseTimeval(info.facts["create"]); } return info; } public FtpFileInfo getFileInfo(const(char)[] path) in { assert(path.length > 0); } body { // Start assuming the MLST didn't work. bool mlst_success = false; FtpResponse response; auto inf = ls(path); if(inf.length == 1) return inf[0]; else { debug(FtpUnitTest) { Stdout("In getFileInfo.").newline.flush; } { // Send a list command. This may list the contents of a directory, even. FtpFileInfo[] temp = this.sendListCommand(path); // If there wasn't at least one line, the file didn't exist? // We should have already handled that. if(temp.length < 1) throw new FtpException( "CLIENT: Bad LIST response from server", "501"); // If there are multiple lines, try to return the correct one. if(temp.length != 1) foreach(FtpFileInfo info; temp) { if(info.type == FtpFileType.cdir) return info; } // Okay then, the first line. Best we can do? return temp[0]; } } } public void put(const(char)[] path, const(char)[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); assert(local_file.length > 0); } body { // Open the file for reading... auto file = new File(local_file); scope(exit) { file.detach(); delete file; } // Seek to the correct place, if specified. if(this.restartPos_ > 0) { file.seek(this.restartPos_); this.restartPos_ = 0; } else { // Allocate space for the file, if we need to. //this.allocate(file.length); } // Now that it's open, we do what we always do. this.put(path, file, progress, format); } /******************************************************************************** Store data from a stream on the server. Calling this function will change the current data transfer format. Params: path = the path to the remote file stream = data to store, or null for a blank file progress = a delegate to call with progress information format = what format to send the data in ********************************************************************************/ public void put(const(char)[] path, InputStream stream = null, FtpProgress progress = null, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); } body { // Change to the specified format. this.type(format); // Okay server, we want to store something... Socket data = this.processDataCommand("STOR", path); // Send the stream over the socket! if(stream !is null) this.sendStream(data, stream, progress); this.finishDataCommand(data); } /******************************************************************************** Append data to a file on the server. Calling this function will change the current data transfer format. Params: path = the path to the remote file stream = data to append to the file progress = a delegate to call with progress information format = what format to send the data in ********************************************************************************/ public void append(const(char)[] path, InputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); assert(stream !is null); } body { // Change to the specified format. this.type(format); // Okay server, we want to store something... Socket data = this.processDataCommand("APPE", path); // Send the stream over the socket! this.sendStream(data, stream, progress); this.finishDataCommand(data); } /********************************************************************************* Seek to a byte offset for the next transfer. Params: offset = the number of bytes to seek forward **********************************************************************************/ public void restartSeek(size_t offset) { char[16] tmp; this.sendCommand("REST", Integer.format(tmp, cast(long) offset)); this.readResponse("350"); // Set this for later use. this.restartPos_ = offset; } /********************************************************************************** Allocate space for a file. After calling this, append() or put() should be the next command. Params: bytes = the number of bytes to allocate ***********************************************************************************/ public void allocate(long bytes) in { assert(bytes > 0); } body { char[16] tmp; this.sendCommand("ALLO", Integer.format(tmp, bytes)); auto response = this.readResponse(); // For our purposes 200 and 202 are both fine. if(response.code != "200" && response.code != "202") exception(response); } /********************************************************************************** Retrieve a remote file's contents into a local file. Calling this function will change the current data transfer format. Params: path = the path to the remote file local_file = the path to the local file progress = a delegate to call with progress information format = what format to read the data in **********************************************************************************/ public void get(const(char)[] path, const(char)[] local_file, FtpProgress progress = null, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); assert(local_file.length > 0); } body { File file = null; // We may either create a new file... if(this.restartPos_ == 0) file = new File(local_file, File.ReadWriteCreate); // Or open an existing file, and seek to the specified position (read: not end, necessarily.) else { file = new File(local_file, File.ReadWriteExisting); file.seek(this.restartPos_); this.restartPos_ = 0; } scope(exit) { file.detach(); delete file; } // Now that it's open, we do what we always do. this.get(path, file, progress, format); } /********************************************************************************* Enable UTF8 on servers that don't use this as default. Might need some work *********************************************************************************/ public void enableUTF8() { sendCommand("OPTS UTF8 ON"); readResponse("200"); } /********************************************************************************** Retrieve a remote file's contents into a local file. Calling this function will change the current data transfer format. Params: path = the path to the remote file stream = stream to write the data to progress = a delegate to call with progress information format = what format to read the data in ***********************************************************************************/ public void get(const(char)[] path, OutputStream stream, FtpProgress progress = null, FtpFormat format = FtpFormat.image) in { assert(path.length > 0); assert(stream !is null); } body { // Change to the specified format. this.type(format); // Okay server, we want to get this file... Socket data = this.processDataCommand("RETR", path); // Read the stream in from the socket! this.readStream(data, stream, progress); this.finishDataCommand(data); } /***************************************************************************** Added Since: 0.99.8 *****************************************************************************/ public InputStream input(const(char)[] path) { type(FtpFormat.image); dataSocket_ = this.processDataCommand("RETR", path); return dataSocket_; } /***************************************************************************** Added Since: 0.99.8 *****************************************************************************/ public OutputStream output(const(char)[] path) { type(FtpFormat.image); dataSocket_ = this.processDataCommand("STOR", path); return dataSocket_; } } |