]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/io/BamHttp_p.cpp
Stablized HTTP access on all platforms. (issue #54, issue #11)
[bamtools.git] / src / api / internal / io / BamHttp_p.cpp
1 // ***************************************************************************
2 // BamHttp_p.cpp (c) 2011 Derek Barnett
3 // Marth Lab, Department of Biology, Boston College
4 // ---------------------------------------------------------------------------
5 // Last modified: 24 July 2013 (DB)
6 // ---------------------------------------------------------------------------
7 // Provides reading/writing of BAM files on HTTP server
8 // ***************************************************************************
9
10 #include "api/BamAux.h"
11 #include "api/internal/io/BamHttp_p.h"
12 #include "api/internal/io/HttpHeader_p.h"
13 #include "api/internal/io/TcpSocket_p.h"
14 using namespace BamTools;
15 using namespace BamTools::Internal;
16
17 #include <cassert>
18 #include <cctype>
19 #include <cstdlib>
20 #include <algorithm>
21 #include <sstream>
22 using namespace std;
23
24 namespace BamTools {
25 namespace Internal {
26
27 // -----------
28 // constants
29 // -----------
30
31 static const string HTTP_PORT   = "80";
32 static const string HTTP_PREFIX = "http://";
33 static const size_t HTTP_PREFIX_LENGTH = 7;
34
35 static const string DOUBLE_NEWLINE = "\n\n";
36
37 static const string GET_METHOD   = "GET";
38 static const string HEAD_METHOD  = "HEAD";
39 static const string HOST_HEADER  = "Host";
40 static const string RANGE_HEADER = "Range";
41 static const string BYTES_PREFIX = "bytes=";
42 static const string CONTENT_LENGTH_HEADER = "Content-Length";
43
44 static const char HOST_SEPARATOR  = '/';
45 static const char PROXY_SEPARATOR = ':';
46
47 // -----------------
48 // utility methods
49 // -----------------
50
51 static inline
52 bool endsWith(const string& source, const string& pattern) {
53     return ( source.find(pattern) == (source.length() - pattern.length()) );
54 }
55
56 static inline
57 string toLower(const string& s) {
58     string out;
59     const size_t sSize = s.size();
60     out.reserve(sSize);
61     for ( size_t i = 0; i < sSize; ++i )
62         out[i] = tolower(s[i]);
63     return out;
64 }
65
66 } // namespace Internal
67 } // namespace BamTools
68
69 // ------------------------
70 // BamHttp implementation
71 // ------------------------
72
73 BamHttp::BamHttp(const string& url)
74     : IBamIODevice()
75     , m_socket(new TcpSocket)
76     , m_port(HTTP_PORT)
77     , m_request(0)
78     , m_response(0)
79     , m_isUrlParsed(false)
80     , m_filePosition(-1)
81     , m_fileEndPosition(-1)
82     , m_rangeEndPosition(-1)
83 {
84     ParseUrl(url);
85 }
86
87 BamHttp::~BamHttp(void) {
88
89     // close connection & clean up
90     Close();
91     if ( m_socket )
92         delete m_socket;
93 }
94
95 void BamHttp::ClearResponse(void) {
96     if ( m_response ) {
97         delete m_response;
98         m_response = 0;
99     }
100 }
101
102 void BamHttp::Close(void) {
103
104     // disconnect socket & clear related resources
105     DisconnectSocket();
106
107     // reset state
108     m_isUrlParsed = false;
109     m_filePosition     = -1;
110     m_fileEndPosition  = -1;
111     m_rangeEndPosition = -1;
112     m_mode = IBamIODevice::NotOpen;
113 }
114
115 bool BamHttp::ConnectSocket(void) {
116
117     BT_ASSERT_X(m_socket, "null socket?");
118
119     // any state checks, etc?
120     if ( !m_socket->ConnectToHost(m_hostname, m_port, m_mode) ) {
121         SetErrorString("BamHttp::ConnectSocket", m_socket->GetErrorString());
122         return false;
123     }
124
125     // return success
126     return true;
127 }
128
129 void BamHttp::DisconnectSocket(void) {
130
131     // disconnect socket & clean up
132     m_socket->DisconnectFromHost();
133     ClearResponse();
134     if ( m_request )  {
135         delete m_request;
136         m_request = 0;
137     }
138 }
139
140 bool BamHttp::EnsureSocketConnection(void) {
141     if ( m_socket->IsConnected() )
142         return true;
143     return ConnectSocket();
144 }
145
146 bool BamHttp::IsOpen(void) const {
147     return IBamIODevice::IsOpen() && m_isUrlParsed;
148 }
149
150 bool BamHttp::IsRandomAccess(void) const {
151     return true;
152 }
153
154 bool BamHttp::Open(const IBamIODevice::OpenMode mode) {
155
156     // BamHttp only supports read-only access
157     if ( mode != IBamIODevice::ReadOnly ) {
158         SetErrorString("BamHttp::Open", "writing on this device is not supported");
159         return false;
160     }
161     m_mode = mode;
162
163     // attempt connection to socket
164     if ( !ConnectSocket() ) {
165         SetErrorString("BamHttp::Open", m_socket->GetErrorString());
166         return false;
167     }
168
169     // initialize our file positions
170     m_filePosition     = 0;
171     m_fileEndPosition  = 0;
172     m_rangeEndPosition = 0;
173
174     // attempt to send initial request (just 'HEAD' to check connection)
175     if ( !SendHeadRequest() ) {
176         SetErrorString("BamHttp::Open", m_socket->GetErrorString());
177         return false;
178     }
179
180     // clear response from HEAD request, not needed
181     ClearResponse();
182
183     // return success
184     return true;
185 }
186
187 void BamHttp::ParseUrl(const string& url) {
188
189     // clear flag to start
190     m_isUrlParsed = false;
191
192     // make sure url starts with "http://", case-insensitive
193     string tempUrl(url);
194     toLower(tempUrl);
195     const size_t prefixFound = tempUrl.find(HTTP_PREFIX);
196     if ( prefixFound == string::npos )
197         return;
198
199     // find end of host name portion (first '/' hit after the prefix)
200     const size_t firstSlashFound = tempUrl.find(HOST_SEPARATOR, HTTP_PREFIX_LENGTH);
201     if ( firstSlashFound == string::npos ) {
202         ;  // no slash found... no filename given along with host?
203     }
204
205     // fetch hostname (check for proxy port)
206     string hostname = tempUrl.substr(HTTP_PREFIX_LENGTH, (firstSlashFound - HTTP_PREFIX_LENGTH));
207     const size_t colonFound = hostname.find(PROXY_SEPARATOR);
208     if ( colonFound != string::npos ) {
209         ; // TODO: handle proxy port (later, just skip for now)
210     } else {
211         m_hostname = hostname;
212         m_port = HTTP_PORT;
213     }
214
215     // store remainder of URL as filename (must be non-empty)
216     string filename = tempUrl.substr(firstSlashFound);
217     if ( filename.empty() )
218         return;
219     m_filename = filename;
220
221     // set parsed OK flag
222     m_isUrlParsed = true;
223 }
224
225 int64_t BamHttp::Read(char* data, const unsigned int numBytes) {
226
227     // if BamHttp not in a valid state
228     if ( !IsOpen() )
229         return -1;
230
231     int64_t numBytesReadSoFar = 0;
232     while ( numBytesReadSoFar < numBytes ) {
233
234         const size_t remaining = static_cast<size_t>( numBytes - numBytesReadSoFar );
235
236         // if we're not holding a valid GET reponse, get one
237         if ( m_response == 0 ) {
238             if ( !SendGetRequest(remaining) )
239                 return -1;
240         }
241         BT_ASSERT_X(m_response, "null HTTP response");
242
243         // check response status code
244         const int statusCode = m_response->GetStatusCode();
245
246         // if we receieved full file contents in response
247         if ( statusCode == 200 ) {
248
249             // try to read 'remaining' bytes from socket
250             const int64_t socketBytesRead = ReadFromSocket(data+numBytesReadSoFar, remaining);
251
252             // if error
253             if ( socketBytesRead < 0 ) {
254                 SetErrorString("BamHttp::Read", m_socket->GetErrorString());
255                 return -1;
256             }
257
258             // EOF
259             else if ( socketBytesRead == 0 )
260                 return numBytesReadSoFar;
261
262             // update counters
263             numBytesReadSoFar += socketBytesRead;
264             m_filePosition    += socketBytesRead;
265
266         }
267
268         // else if we received a range of bytes in response
269         else if ( statusCode == 206 ) {
270
271             // if we've exhausted the last request
272             if ( m_filePosition == m_rangeEndPosition ) {
273                 if ( !SendGetRequest(remaining) )
274                     return -1;
275             }
276
277             else {
278
279                 // try to read 'remaining' bytes from socket
280                 const int64_t socketBytesRead = ReadFromSocket(data+numBytesReadSoFar, remaining);
281
282                 // if error
283                 if ( socketBytesRead < 0 ) {
284                     SetErrorString("BamHttp::Read", m_socket->GetErrorString());
285                     return -1;
286                 }
287
288                 // maybe EOF
289                 else if ( socketBytesRead == 0 ) {
290
291                     // if we know we're not at end position, fire off a new request
292                     if ( m_fileEndPosition > 0 && m_filePosition < m_fileEndPosition ) {
293                         if ( !SendGetRequest() )
294                             return -1;
295                     } else
296                         return numBytesReadSoFar;
297                 }
298
299                 // update counters
300                 numBytesReadSoFar += socketBytesRead;
301                 m_filePosition    += socketBytesRead;
302             }
303         }
304
305
306         // else some other HTTP status
307         else {
308             SetErrorString("BamHttp::Read", "unsupported status code in response");
309             return -1;
310         }
311     }
312
313     // return actual number of bytes read
314     return numBytesReadSoFar;
315 }
316
317 int64_t BamHttp::ReadFromSocket(char* data, const unsigned int maxNumBytes) {
318     return m_socket->Read(data, maxNumBytes);
319 }
320
321 bool BamHttp::ReceiveResponse(void) {
322
323     // fetch header, up until double new line
324     string responseHeader;
325     do {
326
327         // make sure we can read a line
328         if ( !m_socket->WaitForReadLine() )
329             return false;
330
331         // read line & append to full header
332         const string headerLine = m_socket->ReadLine();
333         responseHeader += headerLine;
334
335     } while ( !endsWith(responseHeader, DOUBLE_NEWLINE) );
336
337     // sanity check
338     if ( responseHeader.empty() ) {
339         SetErrorString("BamHttp::ReceiveResponse", "empty HTTP response");
340         Close();
341         return false;
342     }
343
344     // create response from header text
345     m_response = new HttpResponseHeader(responseHeader);
346     if ( !m_response->IsValid() ) {
347         SetErrorString("BamHttp::ReceiveResponse", "could not parse HTTP response");
348         Close();
349         return false;
350     }
351
352     // if we get here, success
353     return true;
354 }
355
356 bool BamHttp::Seek(const int64_t& position, const int origin) {
357
358     // if HTTP device not in a valid state
359     if ( !IsOpen() ) {
360         SetErrorString("BamHttp::Seek", "cannot seek on unopen connection");
361         return false;
362     }
363
364     // reset the connection
365     DisconnectSocket();
366     if ( !ConnectSocket() ) {
367         SetErrorString("BamHttp::Seek", m_socket->GetErrorString());
368         return false;
369     }
370
371     // udpate file position
372     switch ( origin ) {
373         case SEEK_CUR : m_filePosition += position; break;
374         case SEEK_SET : m_filePosition  = position; break;
375         default :
376             SetErrorString("BamHttp::Seek", "unsupported seek origin");
377             return false;
378     }
379
380     // return success
381     return true;
382 }
383
384 bool BamHttp::SendGetRequest(const size_t numBytes) {
385
386     // clear previous data
387     ClearResponse();
388     if ( m_request )
389         delete m_request;
390     m_socket->ClearBuffer();
391
392     // make sure we're connected
393     if ( !EnsureSocketConnection() )
394         return false;
395
396     // create range string
397     const int64_t endPosition = m_filePosition + std::max(static_cast<size_t>(0x10000), numBytes);
398     stringstream range("");
399     range << BYTES_PREFIX << m_filePosition << '-' << endPosition;
400
401     // create request
402     m_request = new HttpRequestHeader(GET_METHOD, m_filename);
403     m_request->SetField(HOST_HEADER,  m_hostname);
404     m_request->SetField(RANGE_HEADER, range.str());
405
406     // send request
407     const string requestHeader = m_request->ToString();
408     const size_t headerSize    = requestHeader.size();
409     if ( WriteToSocket(requestHeader.c_str(), headerSize) != headerSize ) {
410         SetErrorString("BamHttp::SendHeadRequest", m_socket->GetErrorString());
411         return false;
412     }
413
414     // ensure clean buffer
415     m_socket->ClearBuffer();
416
417     // wait for response
418     if ( !ReceiveResponse() ) {
419         SetErrorString("BamHttp::SendGetRequest", m_socket->GetErrorString());
420         Close();
421         return false;
422     }
423     BT_ASSERT_X(m_response, "BamHttp::SendGetRequest : null HttpResponse");
424     BT_ASSERT_X(m_response->IsValid(), "BamHttp::SendGetRequest : invalid HttpResponse");
425
426     // check response status code
427     const int statusCode = m_response->GetStatusCode();
428     switch ( statusCode ) {
429
430         // ranged response, as requested
431         case 206 :
432             // get content length if available
433             if ( m_response->ContainsKey(CONTENT_LENGTH_HEADER) ) {
434                 const string contentLengthString = m_response->GetValue(CONTENT_LENGTH_HEADER);
435                 m_rangeEndPosition = m_filePosition + atoi( contentLengthString.c_str() );
436             }
437             return true;
438
439         // full contents, not range
440         case 200 :
441         {
442             // skip up to current file position
443             RaiiBuffer tmp(0x8000);
444             int64_t numBytesRead = 0;
445             while ( numBytesRead < m_filePosition ) {
446
447                 // read data from response
448                 const int64_t remaining = m_filePosition - numBytesRead;
449                 const size_t bytesToRead = static_cast<size_t>( (remaining > 0x8000) ? 0x8000 : remaining );
450                 const int64_t socketBytesRead = ReadFromSocket(tmp.Buffer, bytesToRead);
451
452                 // if error
453                 if ( socketBytesRead < 0 ) {
454                     SetErrorString("BamHttp::SendGetRequest", m_socket->GetErrorString());
455                     Close();
456                     return false;
457                 }
458
459                 // else if EOF
460                 else if ( socketBytesRead == 0 && m_socket->BufferBytesAvailable() == 0 )
461                     break;
462
463                 // update byte counter
464                 numBytesRead += socketBytesRead;
465             }
466
467             // return success
468             return ( numBytesRead == m_filePosition);
469         }
470
471         // any other status codes
472         default:
473             break;
474     }
475
476     // fail on unexpected status code
477     SetErrorString("BamHttp::SendGetRequest", "unsupported status code in response");
478     Close();
479     return false;
480 }
481
482 bool BamHttp::SendHeadRequest(void) {
483
484     // ensure clean slate
485     ClearResponse();
486     if ( m_request )
487         delete m_request;
488     m_socket->ClearBuffer();
489
490     // make sure we're connected
491     if ( !EnsureSocketConnection() )
492         return false;
493
494     // create request
495     m_request = new HttpRequestHeader(HEAD_METHOD, m_filename);
496     m_request->SetField(HOST_HEADER, m_hostname);
497
498     // send request
499     const string requestHeader = m_request->ToString();
500     const size_t headerSize    = requestHeader.size();
501     if ( WriteToSocket(requestHeader.c_str(), headerSize) != headerSize ) {
502         SetErrorString("BamHttp::SendHeadRequest", m_socket->GetErrorString());
503         return false;
504     }
505
506     m_socket->ClearBuffer();
507
508     // wait for response from server
509     if ( !ReceiveResponse() ) {
510         SetErrorString("BamHttp::SendHeadRequest", m_socket->GetErrorString());
511         Close();
512         return false;
513     }
514     BT_ASSERT_X(m_response, "BamHttp::SendHeadRequest : null HttpResponse");
515     BT_ASSERT_X(m_response->IsValid(), "BamHttp::SendHeadRequest : invalid HttpResponse");
516
517     // get content length if available
518     if ( m_response->ContainsKey(CONTENT_LENGTH_HEADER) ) {
519         const string contentLengthString = m_response->GetValue(CONTENT_LENGTH_HEADER);
520         m_fileEndPosition = atoi( contentLengthString.c_str() ) - 1;
521     }
522
523     // return whether we found any errors
524     return m_socket->GetError() == TcpSocket::NoError;
525 }
526
527 int64_t BamHttp::Tell(void) const {
528     return ( IsOpen() ? m_filePosition : -1 );
529 }
530
531 int64_t BamHttp::Write(const char* data, const unsigned int numBytes) {
532     (void)data;
533     (void)numBytes;
534     BT_ASSERT_X(false, "BamHttp::Write : write-mode not supported on this device");
535     SetErrorString("BamHttp::Write", "write-mode not supported on this device");
536     return -1;
537 }
538
539 int64_t BamHttp::WriteToSocket(const char* data, const unsigned int numBytes) {
540     if ( !m_socket->IsConnected() )
541         return -1;
542     m_socket->ClearBuffer();
543     return m_socket->Write(data, numBytes);
544 }