]> git.donarmstrong.com Git - bamtools.git/blob - src/api/internal/io/BamHttp_p.cpp
Updated file headers (filename, license, description, etc)
[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: 10 November 2011 (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 <algorithm>
20 #include <sstream>
21 using namespace std;
22
23 namespace BamTools {
24 namespace Internal {
25
26 // -----------
27 // constants
28 // -----------
29
30 static const string HTTP_PORT   = "80";
31 static const string HTTP_PREFIX = "http://";
32 static const size_t HTTP_PREFIX_LENGTH = 7;
33
34 static const string DOUBLE_NEWLINE = "\n\n";
35
36 static const string GET_METHOD   = "GET";
37 static const string HOST_HEADER  = "Host";
38 static const string RANGE_HEADER = "Range";
39 static const string BYTES_PREFIX = "bytes=";
40
41 static const char HOST_SEPARATOR  = '/';
42 static const char PROXY_SEPARATOR = ':';
43
44 // -----------------
45 // utility methods
46 // -----------------
47
48 static inline
49 bool endsWith(const string& source, const string& pattern) {
50     return ( source.find(pattern) == (source.length() - pattern.length()) );
51 }
52
53 static inline
54 string toLower(const string& s) {
55     string out;
56     const size_t sSize = s.size();
57     out.reserve(sSize);
58     for ( size_t i = 0; i < sSize; ++i )
59         out[i] = tolower(s[i]);
60     return out;
61 }
62
63 } // namespace Internal
64 } // namespace BamTools
65
66 // ------------------------
67 // BamHttp implementation
68 // ------------------------
69
70 BamHttp::BamHttp(const string& url)
71     : IBamIODevice()
72     , m_socket(new TcpSocket)
73     , m_port(HTTP_PORT)
74     , m_request(0)
75     , m_response(0)
76     , m_isUrlParsed(false)
77     , m_filePosition(-1)
78     , m_endRangeFilePosition(-1)
79 {
80     ParseUrl(url);
81 }
82
83 BamHttp::~BamHttp(void) {
84
85     // close connection & clean up
86     Close();
87     if ( m_socket )
88         delete m_socket;
89 }
90
91 void BamHttp::Close(void) {
92
93     // disconnect socket
94     m_socket->DisconnectFromHost();
95
96     // clean up request & response
97     if ( m_request )  {
98         delete m_request;
99         m_request = 0;
100     }
101     if ( m_response ) {
102         delete m_response;
103         m_response = 0;
104     }
105
106     // reset state - necessary??
107     m_isUrlParsed = false;
108     m_filePosition = -1;
109     m_endRangeFilePosition = -1;
110 }
111
112 bool BamHttp::ConnectSocket(void) {
113
114     BT_ASSERT_X(m_socket, "null socket?");
115
116     // any state checks, etc?
117     if ( !m_socket->ConnectToHost(m_hostname, m_port, m_mode) ) {
118         // TODO: set error string
119         return false;
120     }
121
122     // attempt initial request
123     m_filePosition = 0;
124     m_endRangeFilePosition = -1;
125     if ( !SendRequest() ) {
126         // TODO: set error string
127         Close();
128         return false;
129     }
130
131     // wait for response from server
132     if ( !ReceiveResponse() ) {
133         // TODO: set error string
134         Close();
135         return false;
136     }
137
138     // return success
139     return true;
140 }
141
142 bool BamHttp::EnsureSocketConnection(void) {
143     if ( m_socket->IsConnected() )
144         return true;
145     else return ConnectSocket();
146 }
147
148 bool BamHttp::IsOpen(void) const {
149     return IBamIODevice::IsOpen() && m_isUrlParsed;
150 }
151
152 bool BamHttp::IsRandomAccess(void) const {
153     return true;
154 }
155
156 bool BamHttp::Open(const IBamIODevice::OpenMode mode) {
157
158     // BamHttp only supports read-only access
159     if ( mode != IBamIODevice::ReadOnly ) {
160         SetErrorString("BamHttp::Open", "writing on this device is not supported");
161         return false;
162     }
163     m_mode = mode;
164
165     // attempt connection to socket
166     if ( !ConnectSocket() ) {
167         SetErrorString("BamHttp::Open", m_socket->GetErrorString());
168         return false;
169     }
170
171     // return success
172     return true;
173 }
174
175 void BamHttp::ParseUrl(const string& url) {
176
177     // clear flag to start
178     m_isUrlParsed = false;
179
180     // make sure url starts with "http://", case-insensitive
181     string tempUrl(url);
182     toLower(tempUrl);
183     const size_t prefixFound = tempUrl.find(HTTP_PREFIX);
184     if ( prefixFound == string::npos )
185         return;
186
187     // find end of host name portion (first '/' hit after the prefix)
188     const size_t firstSlashFound = tempUrl.find(HOST_SEPARATOR, HTTP_PREFIX_LENGTH);
189     if ( firstSlashFound == string::npos ) {
190         ;  // no slash found... no filename given along with host?
191     }
192
193     // fetch hostname (check for proxy port)
194     string hostname = tempUrl.substr(HTTP_PREFIX_LENGTH, (firstSlashFound - HTTP_PREFIX_LENGTH));
195     const size_t colonFound = hostname.find(PROXY_SEPARATOR);
196     if ( colonFound != string::npos ) {
197         ; // TODO: handle proxy port (later, just skip for now)
198     } else {
199         m_hostname = hostname;
200         m_port = HTTP_PORT;
201     }
202
203     // store remainder of URL as filename (must be non-empty)
204     string filename = tempUrl.substr(firstSlashFound);
205     if ( filename.empty() )
206         return;
207     m_filename = filename;
208
209     // set parsed OK flag
210     m_isUrlParsed = true;
211 }
212
213 int64_t BamHttp::Read(char* data, const unsigned int numBytes) {
214
215     // if BamHttp not in a valid state
216     if ( !IsOpen() )
217         return -1;
218
219     // read until hit desired @numBytes
220     int64_t bytesReadSoFar = 0;
221     while ( bytesReadSoFar < numBytes ) {
222
223         // calculate number of bytes we're going to try to read this iteration
224         const size_t remainingBytes = ( numBytes - bytesReadSoFar );
225
226         // if socket has access to entire file contents
227         // i.e. we received response with full data (status code == 200)
228         if ( m_endRangeFilePosition < 0 ) {
229
230             // try to read 'remainingBytes' from socket
231             const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, remainingBytes);
232             if ( socketBytesRead < 0 )
233                 return -1;
234             bytesReadSoFar += socketBytesRead;
235             m_filePosition += socketBytesRead;
236         }
237
238         // socket has access to a range of data (might already be in buffer)
239         // i.e. we received response with partial data (status code == 206)
240         else {
241
242             // there is data left from last request
243             if ( m_endRangeFilePosition > m_filePosition ) {
244
245                 // try to read either the total 'remainingBytes' or
246                 // whatever we have remaining from last request range
247                 const size_t rangeRemainingBytes = m_endRangeFilePosition - m_filePosition;
248                 const size_t bytesToRead = std::min(remainingBytes, rangeRemainingBytes);
249                 const int64_t socketBytesRead = ReadFromSocket(data+bytesReadSoFar, bytesToRead);
250                 if ( socketBytesRead < 0 )
251                     return -1;
252                 bytesReadSoFar += socketBytesRead;
253                 m_filePosition += socketBytesRead;
254             }
255
256             // otherwise, this is a 1st-time read or
257             // we already read everything from the last GET request
258             else {
259
260                 // request for next range
261                 if ( !SendRequest(remainingBytes) || !ReceiveResponse() ) {
262                     Close();
263                     return -1;
264                 }
265             }
266         }
267     }
268
269     // return actual number bytes successfully read
270     return bytesReadSoFar;
271 }
272
273 int64_t BamHttp::ReadFromSocket(char* data, const unsigned int maxNumBytes) {
274
275     // try to read 'remainingBytes' from socket
276     const int64_t numBytesRead = m_socket->Read(data, maxNumBytes);
277     if ( numBytesRead < 0 )
278         return -1;
279     return numBytesRead;
280 }
281
282 bool BamHttp::ReceiveResponse(void) {
283
284     // clear any prior response
285     if ( m_response )
286         delete m_response;
287
288     // make sure we're connected
289     if ( !EnsureSocketConnection() )
290         return false;
291
292     // fetch header, up until double new line
293     string responseHeader;
294     do {
295         // read line & append to full header
296         const string headerLine = m_socket->ReadLine();
297         responseHeader += headerLine;
298
299     } while ( !endsWith(responseHeader, DOUBLE_NEWLINE) );
300
301     // sanity check
302     if ( responseHeader.empty() ) {
303         // TODO: set error string
304         Close();
305         return false;
306     }
307
308     // create response from header text
309     m_response = new HttpResponseHeader(responseHeader);
310     if ( !m_response->IsValid() ) {
311         // TODO: set error string
312         Close();
313         return false;
314     }
315
316     // if we got range response as requested
317     if ( m_response->GetStatusCode() == 206 )
318         return true;
319
320     // if we got the full file contents instead of range
321     else if ( m_response->GetStatusCode() == 200 ) {
322
323         // skip up to current file position
324         RaiiBuffer tmp(0x8000);
325         int64_t numBytesRead = 0;
326         while ( numBytesRead < m_filePosition ) {
327             int64_t result = ReadFromSocket(tmp.Buffer, 0x8000);
328             if ( result < 0 ) {
329                 Close();
330                 return false;
331             }
332             numBytesRead += result;
333         }
334
335         // return success
336         return true;
337     }
338
339     // on any other reponse status
340     // TODO: set error string
341     Close();
342     return false;
343 }
344
345 bool BamHttp::Seek(const int64_t& position, const int origin) {
346
347     // if HTTP device not in a valid state
348     if ( !IsOpen() ) {
349         // TODO: set error string
350         return false;
351     }
352
353     // discard socket's buffer contents, update positions, & return success
354     m_socket->ClearBuffer();
355
356     if ( origin == SEEK_CUR )
357         m_filePosition += position;
358     else if ( origin == SEEK_SET )
359         m_filePosition = position;
360     else {
361         // TODO: set error string
362         return false;
363     }
364     m_endRangeFilePosition = m_filePosition;
365     return true;
366 }
367
368 bool BamHttp::SendRequest(const size_t numBytes) {
369
370     // remove any currently active request
371     if ( m_request )
372         delete m_request;
373
374     // create range string
375     m_endRangeFilePosition = m_filePosition + numBytes;
376     stringstream range("");
377     range << BYTES_PREFIX << m_filePosition << '-' << m_endRangeFilePosition;
378
379     // make sure we're connected
380     if ( !EnsureSocketConnection() )
381         return false;
382
383     // create request
384     m_request = new HttpRequestHeader(GET_METHOD, m_filename);
385     m_request->SetField(HOST_HEADER,  m_hostname);
386     m_request->SetField(RANGE_HEADER, range.str());
387
388     // write request to socket
389     const string requestHeader = m_request->ToString();
390     const size_t headerSize    = requestHeader.size();
391     return ( WriteToSocket(requestHeader.c_str(), headerSize) == headerSize );
392 }
393
394 int64_t BamHttp::Tell(void) const {
395     return ( IsOpen() ? m_filePosition : -1 );
396 }
397
398 int64_t BamHttp::Write(const char* data, const unsigned int numBytes) {
399     (void)data;
400     (void)numBytes;
401     BT_ASSERT_X(false, "BamHttp::Write : write-mode not supported on this device");
402     SetErrorString("BamHttp::Write", "write-mode not supported on this device");
403     return -1;
404 }
405
406 int64_t BamHttp::WriteToSocket(const char* data, const unsigned int numBytes) {
407     if ( !m_socket->IsConnected() )
408         return -1;
409     m_socket->ClearBuffer();
410     return m_socket->Write(data, numBytes);
411 }