]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/io/BamFtp_p.cpp
Fixed EOF issues on *nix platforms
[bamtools.git] / src / api / internal / io / BamFtp_p.cpp
1 // ***************************************************************************
2 // BamFtp_p.cpp (c) 2011 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // ---------------------------------------------------------------------------
5 // Last modified: 10 November 2011 (DB)
6 // ---------------------------------------------------------------------------
7 // Provides reading/writing of BAM files on FTP server
8 // ***************************************************************************
9
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamFtp_p.h"
12 #include "api/internal/io/TcpSocket_p.h"
13 using namespace BamTools;
14 using namespace BamTools::Internal;
15
16 #include <cctype>
17 #include <cstdlib>
18 #include <sstream>
19 #include <vector>
20 using namespace std;
21
22 namespace BamTools {
23 namespace Internal {
24
25 // -----------
26 // constants
27 // -----------
28
29 static const uint16_t FTP_PORT          = 21;
30 static const string   FTP_PREFIX        = "ftp://";
31 static const size_t   FTP_PREFIX_LENGTH = 6;
32 static const string   FTP_NEWLINE       = "\r\n";
33
34 static const string DEFAULT_USER = "anonymous";
35 static const string DEFAULT_PASS = "anonymous@";
36
37 static const string ABOR_CMD = "ABOR";
38 static const string USER_CMD = "USER";
39 static const string PASS_CMD = "PASS";
40 static const string PASV_CMD = "PASV";
41 static const string REIN_CMD = "REIN";
42 static const string REST_CMD = "REST";
43 static const string RETR_CMD = "RETR";
44 static const string TYPE_CMD = "TYPE";
45
46 static const char CMD_SEPARATOR  = ' ';
47 static const char HOST_SEPARATOR = '/';
48 static const char IP_SEPARATOR   = '.';
49
50 static const char MULTILINE_CONTINUE = '-';
51
52 static const char PASV_REPLY_PREFIX    = '(';
53 static const char PASV_REPLY_SEPARATOR = ',';
54 static const char PASV_REPLY_SUFFIX    = ')';
55
56 // -----------------
57 // utility methods
58 // -----------------
59
60 static inline
61 vector<string> split(const string& source, const char delim) {
62
63     stringstream ss(source);
64     string field;
65     vector<string> fields;
66
67     while ( getline(ss, field, delim) )
68         fields.push_back(field);
69     return fields;
70 }
71
72 static inline
73 bool startsWith(const string& source, const string& pattern) {
74     return ( source.find(pattern) == 0 );
75 }
76
77 static inline
78 string toLower(const string& s) {
79     string out;
80     const size_t sSize = s.size();
81     out.reserve(sSize);
82     for ( size_t i = 0; i < sSize; ++i )
83         out[i] = tolower(s[i]);
84     return out;
85 }
86
87 } // namespace Internal
88 } // namespace BamTools
89
90 // -----------------------
91 // BamFtp implementation
92 // -----------------------
93
94 BamFtp::BamFtp(const string& url)
95     : IBamIODevice()
96     , m_commandSocket(new TcpSocket)
97     , m_dataSocket(new TcpSocket)
98     , m_port(FTP_PORT)
99     , m_dataPort(0)
100     , m_username(DEFAULT_USER)
101     , m_password(DEFAULT_PASS)
102     , m_isUrlParsed(false)
103     , m_filePosition(-1)
104 {
105     ParseUrl(url);
106 }
107
108 BamFtp::~BamFtp(void) {
109
110     // close connection & clean up
111     Close();
112     if ( m_commandSocket )
113         delete m_commandSocket;
114     if ( m_dataSocket )
115         delete m_dataSocket;
116 }
117
118 void BamFtp::Close(void) {
119
120     // disconnect socket
121     m_commandSocket->DisconnectFromHost();
122     m_dataSocket->DisconnectFromHost();
123
124     // reset state - necessary??
125     m_isUrlParsed = false;
126     m_filePosition = -1;
127     m_username = DEFAULT_USER;
128     m_password = DEFAULT_PASS;
129     m_dataHostname.clear();
130     m_dataPort = 0;
131 }
132
133 bool BamFtp::ConnectCommandSocket(void) {
134
135     BT_ASSERT_X(m_commandSocket, "null command socket?");
136
137     // connect to FTP server
138     if ( !m_commandSocket->ConnectToHost(m_hostname, m_port, m_mode) ) {
139         SetErrorString("BamFtp::ConnectCommandSocket", "could not connect to host");
140         return false;
141     }
142
143     // receive initial reply from host
144     if ( !ReceiveReply() ) {
145         Close();
146         return false;
147     }
148
149     // send USER command
150     string userCommand = USER_CMD + CMD_SEPARATOR + m_username + FTP_NEWLINE;
151     if ( !SendCommand(userCommand, true) ) {
152         Close();
153         return false;
154     }
155
156     // send PASS command
157     string passwordCommand = PASS_CMD + CMD_SEPARATOR + m_password + FTP_NEWLINE;
158     if ( !SendCommand(passwordCommand, true) ) {
159         Close();
160         return false;
161     }
162
163     // send TYPE command
164     string typeCommand = TYPE_CMD + CMD_SEPARATOR + 'I' + FTP_NEWLINE;
165     if ( !SendCommand(typeCommand, true) ) {
166         Close();
167         return false;
168     }
169
170     // return success
171     return true;
172 }
173
174 bool BamFtp::ConnectDataSocket(void) {
175
176     // failure if can't connect to command socket first
177     if ( !m_commandSocket->IsConnected() ) {
178         if ( !ConnectCommandSocket() )
179             return false;
180     }
181
182     // make sure we're starting with a fresh data channel
183     if ( m_dataSocket->IsConnected() ) 
184         m_dataSocket->DisconnectFromHost();
185
186     // send passive connection command
187     const string passiveCommand = PASV_CMD + FTP_NEWLINE;
188     if ( !SendCommand(passiveCommand, true) ) {
189         // TODO: set error string
190         return false;
191     }
192
193     // retrieve passive connection port
194     if ( !ParsePassiveResponse() ) {
195         // TODO: set error string
196         return false;
197     }
198
199     // set up restart command (tell server where to start fetching bytes from)
200     if ( m_filePosition >= 0 ) {
201
202         stringstream fpStream("");
203         fpStream << m_filePosition;
204         string restartCommand = REST_CMD + CMD_SEPARATOR + fpStream.str() + FTP_NEWLINE;
205         if ( !SendCommand(restartCommand, true) ) {
206             // TODO: set error string
207             return false;
208         }
209     }
210
211     // main file retrieval request
212     string retrieveCommand = RETR_CMD + CMD_SEPARATOR + m_filename + FTP_NEWLINE;
213     if ( !SendCommand(retrieveCommand, false) ) {
214         // TODO: set error string
215         return false;
216     }
217
218     // make data channel connection
219     if ( !m_dataSocket->ConnectToHost(m_dataHostname, m_dataPort) ) {
220         // TODO: set error string
221         return false;
222     }
223
224     // fetch intial reply from server
225     if ( !ReceiveReply() ) {
226         // TODO: set error string
227         m_dataSocket->DisconnectFromHost();
228         return false;
229     }
230     
231     // make sure we have reply code 150 (all good)
232     if ( !startsWith(m_response, "150") ) {
233         // TODO: set error string
234         m_dataSocket->DisconnectFromHost();
235         return false;
236     }
237
238     // return success
239     return true;
240 }
241
242 bool BamFtp::IsOpen(void) const {
243     return IBamIODevice::IsOpen() && m_isUrlParsed;
244 }
245
246 bool BamFtp::IsRandomAccess(void) const {
247     return true;
248 }
249
250 bool BamFtp::Open(const IBamIODevice::OpenMode mode) {
251
252     // BamFtp only supports read-only access
253     if ( mode != IBamIODevice::ReadOnly ) {
254         SetErrorString("BamFtp::Open", "writing on this device is not supported");
255         return false;
256     }
257
258     // initialize basic valid state
259     m_mode = mode;
260     m_filePosition = 0;
261
262     // attempt connection to command & data sockets
263     return ( ConnectCommandSocket() && ConnectDataSocket() );
264 }
265
266 bool BamFtp::ParsePassiveResponse(void) {
267
268     // fail if empty
269     if ( m_response.empty() )
270         return false;
271
272     // find parentheses
273     const size_t leftParenFound  = m_response.find(PASV_REPLY_PREFIX);
274     const size_t rightParenFound = m_response.find(PASV_REPLY_SUFFIX);
275     if ( leftParenFound == string::npos || rightParenFound == string::npos )
276         return false;
277
278     // grab everything between ( should be "h1,h2,h3,h4,p1,p2" )
279     string::const_iterator responseBegin = m_response.begin();
280     const string hostAndPort(responseBegin+leftParenFound+1, responseBegin+rightParenFound);
281
282     // parse into string fields
283     vector<string> fields = split(hostAndPort, PASV_REPLY_SEPARATOR);
284     if ( fields.size() != 6 )
285         return false;
286
287     // fetch passive connection IP
288     m_dataHostname = fields[0] + IP_SEPARATOR +
289                      fields[1] + IP_SEPARATOR +
290                      fields[2] + IP_SEPARATOR +
291                      fields[3];
292
293     // fetch passive connection port
294     const uint8_t portUpper = static_cast<uint8_t>(atoi(fields[4].c_str()));
295     const uint8_t portLower = static_cast<uint8_t>(atoi(fields[5].c_str()));
296     m_dataPort = ( portUpper<<8 ) + portLower;
297
298     // return success
299     return true;
300 }
301
302 void BamFtp::ParseUrl(const string& url) {
303
304     // clear flag to start
305     m_isUrlParsed = false;
306
307     // make sure url starts with "ftp://", case-insensitive
308     string tempUrl(url);
309     toLower(tempUrl);
310     const size_t prefixFound = tempUrl.find(FTP_PREFIX);
311     if ( prefixFound == string::npos )
312         return;
313
314     // find end of host name portion (first '/' hit after the prefix)
315     const size_t firstSlashFound = tempUrl.find(HOST_SEPARATOR, FTP_PREFIX_LENGTH);
316     if ( firstSlashFound == string::npos ) {
317         ;  // no slash found... no filename given along with host?
318     }
319
320     // fetch hostname
321     string hostname = tempUrl.substr(FTP_PREFIX_LENGTH, (firstSlashFound - FTP_PREFIX_LENGTH));
322     m_hostname = hostname;
323     m_port = FTP_PORT;
324
325     // store remainder of URL as filename (must be non-empty)
326     string filename = tempUrl.substr(firstSlashFound);
327     if ( filename.empty() )
328         return;
329     m_filename = filename;
330
331     // set parsed OK flag
332     m_isUrlParsed = true;
333 }
334
335 int64_t BamFtp::Read(char* data, const unsigned int numBytes) {
336
337     // if BamHttp not in a valid state
338     if ( !IsOpen() )
339         return -1;
340
341     // read until hit desired @numBytes
342     int64_t bytesReadSoFar = 0;
343     while ( bytesReadSoFar < numBytes ) {
344
345         // calculate number of bytes we're going to try to read this iteration
346         const size_t remainingBytes = ( numBytes - bytesReadSoFar );
347
348         // if either disconnected somehow, or (more likely) we have seeked since last read
349         if ( !m_dataSocket->IsConnected() ) {
350             if ( !ConnectDataSocket() ) {
351                 // TODO: set error string
352                 return -1;
353             }
354         }
355
356         // read bytes from data socket
357         const int64_t socketBytesRead = ReadDataSocket(data+bytesReadSoFar, remainingBytes);
358         if ( socketBytesRead < 0 ) // error
359             return -1;
360         else if ( socketBytesRead == 0 ) // EOF
361             return bytesReadSoFar;
362         bytesReadSoFar += socketBytesRead;
363         m_filePosition += socketBytesRead;
364     }
365
366     // return actual number bytes successfully read
367     return bytesReadSoFar;
368 }
369
370 int64_t BamFtp::ReadCommandSocket(char* data, const unsigned int maxNumBytes) {
371
372     // try to read 'remainingBytes' from socket
373     const int64_t numBytesRead = m_commandSocket->Read(data, maxNumBytes);
374     if ( numBytesRead < 0 )
375         return -1;
376     return numBytesRead;
377 }
378
379 int64_t BamFtp::ReadDataSocket(char* data, const unsigned int maxNumBytes) {
380
381     // try to read 'remainingBytes' from socket
382     const int64_t numBytesRead = m_dataSocket->Read(data, maxNumBytes);
383     if ( numBytesRead < 0 )
384         return -1;
385     return numBytesRead;
386 }
387
388 bool BamFtp::ReceiveReply(void) {
389
390     // failure if not connected
391     if ( !m_commandSocket->IsConnected() ) {
392         SetErrorString("BamFtp::ReceiveReply()", "command socket not connected");
393         return false;
394     }
395
396     m_response.clear();
397
398     // read header data (& discard for now)
399     bool headerEnd = false;
400     while ( !headerEnd ) {
401
402         const string headerLine = m_commandSocket->ReadLine();
403         m_response += headerLine;
404
405         // if line is of form 'xyz ', quit reading lines
406         if ( (headerLine.length() >= 4 ) &&
407              isdigit(headerLine[0]) &&
408              isdigit(headerLine[1]) &&
409              isdigit(headerLine[2]) &&
410              ( headerLine[3] != MULTILINE_CONTINUE )
411            )
412         {
413             headerEnd = true;
414         }
415     }
416
417     // return success, depending on response
418     if ( m_response.empty() ) {
419         SetErrorString("BamFtp::ReceiveReply", "error reading server reply");
420         return false;
421     }
422     return true;
423 }
424
425 bool BamFtp::Seek(const int64_t& position, const int origin) {
426
427     // if FTP device not in a valid state
428     if ( !IsOpen() ) {
429         // TODO: set error string
430         return false;
431     }
432
433     // ----------------------
434     // UGLY !! but works??
435     // ----------------------
436     // disconnect from server
437     m_dataSocket->DisconnectFromHost();
438     m_commandSocket->DisconnectFromHost();
439
440     // update file position & return success
441     if ( origin == SEEK_CUR )
442         m_filePosition += position;
443     else if ( origin == SEEK_SET)
444         m_filePosition = position;
445     else {
446         // TODO: set error string
447         return false;
448     }
449     return true;
450 }
451
452 bool BamFtp::SendCommand(const string& command, bool waitForReply) {
453
454     // failure if not connected
455     if ( !m_commandSocket->IsConnected() ) {
456         SetErrorString("BamFtp::SendCommand", "command socket not connected");
457         return false;
458     }
459
460     // write command to 'command socket'
461     if ( WriteCommandSocket(command.c_str(), command.length()) == -1 ) {
462         SetErrorString("BamFtp::SendCommand", "error writing to socket");
463         // get actual error from command socket??
464         return false;
465     }
466
467     // if we sent a command that receives a response
468     if ( waitForReply )
469         return ReceiveReply();
470
471     // return success
472     return true;
473 }
474
475 int64_t BamFtp::Tell(void) const {
476     return ( IsOpen() ? m_filePosition : -1 );
477 }
478
479 int64_t BamFtp::Write(const char* data, const unsigned int numBytes) {
480     (void)data;
481     (void)numBytes;
482     BT_ASSERT_X(false, "BamFtp::Write : write-mode not supported on this device");
483     SetErrorString("BamFtp::Write", "write-mode not supported on this device");
484     return -1;
485 }
486
487 int64_t BamFtp::WriteCommandSocket(const char* data, const unsigned int numBytes) {
488     if ( !m_commandSocket->IsConnected() )
489         return -1;
490     m_commandSocket->ClearBuffer();
491     return m_commandSocket->Write(data, numBytes);
492 }
493
494 int64_t BamFtp::WriteDataSocket(const char* data, const unsigned int numBytes) {
495     (void)data;
496     (void)numBytes;
497     BT_ASSERT_X(false, "BamFtp::WriteDataSocket: write-mode not supported on this device");
498     SetErrorString("BamFtp::Write", "write-mode not supported on this device");
499     return -1;
500 }