NetServices Stack source
Dependents: HelloWorld ServoInterfaceBoardExample1 4180_Lab4
services/http/client/HTTPClient.cpp
- Committer:
- donatien
- Date:
- 2010-08-05
- Revision:
- 11:da4498f591ee
- Parent:
- 9:c79fa4034f5b
File content as of revision 11:da4498f591ee:
/* Copyright (c) 2010 Donatien Garnier (donatiengar [at] gmail [dot] com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "core/netservice.h" #include "HTTPClient.h" #include "../util/base64.h" #include "../util/url.h" //#define __DEBUG #include "dbg/dbg.h" #define HTTP_REQUEST_TIMEOUT 30000//15000 #define HTTP_PORT 80 #define CHUNK_SIZE 256 HTTPClient::HTTPClient() : NetService(false) /*Not owned by the pool*/, m_meth(HTTP_GET), m_pCbItem(NULL), m_pCbMeth(NULL), m_pCb(NULL), m_watchdog(), m_timeout(0), m_pDnsReq(NULL), m_server(), m_path(), m_closed(true), m_state(HTTP_CLOSED), m_pDataOut(NULL), m_pDataIn(NULL), m_dataChunked(false), m_dataPos(0), m_dataLen(0), m_httpResponseCode(0), m_blockingResult(HTTP_PROCESSING) { setTimeout(HTTP_REQUEST_TIMEOUT); m_buf = new char[CHUNK_SIZE]; DBG("New HTTPClient %p\n",this); } HTTPClient::~HTTPClient() { close(); delete[] m_buf; } void HTTPClient::basicAuth(const char* user, const char* password) //Basic Authentification { if(user==NULL) { m_reqHeaders.erase("Authorization"); //Remove auth str return; } string auth = "Basic "; string decStr = user; decStr += ":"; decStr += password; auth.append( Base64::encode(decStr) ); DBG("Auth str is %s\n", auth.c_str()); m_reqHeaders["Authorization"] = auth; } //High Level setup functions HTTPResult HTTPClient::get(const char* uri, HTTPData* pDataIn) //Blocking { doGet(uri, pDataIn); return blockingProcess(); } HTTPResult HTTPClient::get(const char* uri, HTTPData* pDataIn, void (*pMethod)(HTTPResult)) //Non blocking { setOnResult(pMethod); doGet(uri, pDataIn); return HTTP_PROCESSING; } #if 0 //For info only template<class T> HTTPResult HTTPClient::get(const char* uri, HTTPData* pDataIn, T* pItem, void (T::*pMethod)(HTTPResult)) //Non blocking { setOnResult(pItem, pMethod); doGet(uri, pDataIn); return HTTP_PROCESSING; } #endif HTTPResult HTTPClient::post(const char* uri, const HTTPData& dataOut, HTTPData* pDataIn) //Blocking { doPost(uri, dataOut, pDataIn); return blockingProcess(); } HTTPResult HTTPClient::post(const char* uri, const HTTPData& dataOut, HTTPData* pDataIn, void (*pMethod)(HTTPResult)) //Non blocking { setOnResult(pMethod); doPost(uri, dataOut, pDataIn); return HTTP_PROCESSING; } #if 0 //For info only template<class T> HTTPResult HTTPClient::post(const char* uri, const HTTPData& dataOut, HTTPData* pDataIn, T* pItem, void (T::*pMethod)(HTTPResult)) //Non blocking { setOnResult(pItem, pMethod); doPost(uri, dataOut, pDataIn); return HTTP_PROCESSING; } #endif void HTTPClient::doGet(const char* uri, HTTPData* pDataIn) { m_meth = HTTP_GET; setup(uri, NULL, pDataIn); } void HTTPClient::doPost(const char* uri, const HTTPData& dataOut, HTTPData* pDataIn) { m_meth = HTTP_POST; setup(uri, (HTTPData*) &dataOut, pDataIn); } void HTTPClient::setOnResult( void (*pMethod)(HTTPResult) ) { m_pCb = pMethod; m_pCbItem = NULL; m_pCbMeth = NULL; } #if 0 //For info only template<class T> void HTTPClient::setOnResult( T* pItem, void (T::*pMethod)(NtpResult) ) { m_pCb = NULL; m_pCbItem = (CDummy*) pItem; m_pCbMeth = (void (CDummy::*)(NtpResult)) pMethod; } #endif void HTTPClient::setTimeout(int ms) { m_timeout = ms; //resetTimeout(); } void HTTPClient::poll() //Called by NetServices { if(m_closed) { return; } if(m_watchdog.read_ms()>m_timeout) { onTimeout(); } else if(m_state == HTTP_READ_DATA_INCOMPLETE) { readData(); //Try to read more data if( m_state == HTTP_DONE ) { //All data has been read, close w/ success :) DBG("Done :)!\n"); onResult(HTTP_OK); close(); } } } int HTTPClient::getHTTPResponseCode() { return m_httpResponseCode; } void HTTPClient::setRequestHeader(const string& header, const string& value) { m_reqHeaders[header] = value; } string& HTTPClient::getResponseHeader(const string& header) { return m_respHeaders[header]; } void HTTPClient::resetRequestHeaders() { m_reqHeaders.clear(); } void HTTPClient::resetTimeout() { m_watchdog.reset(); m_watchdog.start(); } void HTTPClient::init() //Create and setup socket if needed { close(); //Remove previous elements if(!m_closed) //Already opened return; m_state = HTTP_WRITE_HEADERS; m_pTCPSocket = new TCPSocket; m_pTCPSocket->setOnEvent(this, &HTTPClient::onTCPSocketEvent); m_closed = false; m_httpResponseCode = 0; } void HTTPClient::close() { if(m_closed) return; m_state = HTTP_CLOSED; //Now Request headers are kept btw requests unless resetRequestHeaders() is called //m_reqHeaders.clear(); //Clear headers for next requests m_closed = true; //Prevent recursive calling or calling on an object being destructed by someone else m_watchdog.stop(); //Stop timeout m_watchdog.reset(); m_pTCPSocket->resetOnEvent(); m_pTCPSocket->close(); delete m_pTCPSocket; m_pTCPSocket = NULL; if( m_pDnsReq ) { m_pDnsReq->close(); delete m_pDnsReq; m_pDnsReq = NULL; } } void HTTPClient::setup(const char* uri, HTTPData* pDataOut, HTTPData* pDataIn) //Setup request, make DNS Req if necessary { init(); //Initialize client in known state, create socket m_pDataOut = pDataOut; m_pDataIn = pDataIn; resetTimeout(); //Erase previous headers //Do NOT clear m_reqHeaders as they might have already set before connecting m_respHeaders.clear(); //Erase response buffer if(m_pDataIn) m_pDataIn->clear(); //Assert that buffers are initialized properly m_dataLen = 0; m_bufRemainingLen = 0; Url url; url.fromString(uri); m_path = url.getPath(); m_server.setName(url.getHost().c_str()); if( url.getPort() > 0 ) { m_server.setPort( url.getPort() ); } else { m_server.setPort( HTTP_PORT ); } DBG("URL parsed,\r\nHost: %s\r\nPort: %d\r\nPath: %s\n", url.getHost().c_str(), url.getPort(), url.getPath().c_str()); IpAddr ip; if( url.getHostIp(&ip) ) { m_server.setIp(ip); connect(); } else { DBG("DNS Query...\n"); m_pDnsReq = new DNSRequest(); m_pDnsReq->setOnReply(this, &HTTPClient::onDNSReply); m_pDnsReq->resolve(&m_server); DBG("HTTPClient : DNSRequest %p\n", m_pDnsReq); } } void HTTPClient::connect() //Start Connection { resetTimeout(); DBG("Connecting...\n"); m_pTCPSocket->connect(m_server); } #define MIN(a,b) ((a)<(b)?(a):(b)) #define ABS(a) (((a)>0)?(a):0) int HTTPClient::tryRead() //Try to read data from tcp packet and put in the HTTPData object { int len = 0; int readLen; do { if(m_state == HTTP_READ_DATA_INCOMPLETE) //First try to complete buffer copy { readLen = m_bufRemainingLen; /* if (readLen == 0) { m_state = HTTP_READ_DATA; continue; }*/ } else { readLen = m_pTCPSocket->recv(m_buf, MIN(ABS(m_dataLen-m_dataPos),CHUNK_SIZE)); if(readLen < 0) //Error { return readLen; } DBG("%d bytes read\n", readLen); m_pBufRemaining = m_buf; } if (readLen == 0) { m_state = HTTP_READ_DATA; return len; } DBG("Trying to write %d bytes\n", readLen); int writtenLen = m_pDataIn->write(m_pBufRemaining, readLen); m_dataPos += writtenLen; DBG("%d bytes written\n", writtenLen); if(writtenLen<readLen) //Data was not completely written { m_pBufRemaining += writtenLen; m_bufRemainingLen = readLen - writtenLen; m_state = HTTP_READ_DATA_INCOMPLETE; return len + writtenLen; } else { m_state = HTTP_READ_DATA; } len += readLen; } while(readLen>0); return len; } void HTTPClient::readData() //Data has been read { if(m_pDataIn == NULL) //Nothing to read (in HEAD for instance, not supported now) { m_state = HTTP_DONE; return; } DBG("Reading response...\n"); int len = 0; do { if(m_dataChunked && (m_state != HTTP_READ_DATA_INCOMPLETE)) { if(m_dataLen==0) { DBG("Reading chunk length...\n"); //New block static char chunkHeader[16]; //We use m_dataPos to retain the read position in chunkHeader, it has been set to 0 before the first call of readData() m_dataPos += readLine(chunkHeader + m_dataPos, ABS(16 - m_dataPos)); if( m_dataPos > 0 ) { if( chunkHeader[strlen(chunkHeader)-1] == 0x0d ) { sscanf(chunkHeader, "%x%*[^\r\n]", &m_dataLen); DBG("Chunk length is %d\n", m_dataLen); m_dataPos = 0; } else { //Wait for end of line DBG("Wait for CRLF\n"); return; } } else { DBG("Wait for data\n"); //Wait for data return; } } } //Proper data recovery len = tryRead(); if(len<0) //Error { onResult(HTTP_CONN); return; } if(len>0) resetTimeout(); if(m_state == HTTP_READ_DATA_INCOMPLETE) return; //Chunk Tail if(m_dataChunked) { if(m_dataPos >= m_dataLen) { DBG("Chunk read, wait for CRLF\n"); char chunkTail[3]; m_dataPos += readLine(chunkTail, 3); } if(m_dataPos >= m_dataLen + 1) //1 == strlen("\n"), { DBG("End of chunk\n"); if(m_dataLen==0) { DBG("End of file\n"); //End of file m_state = HTTP_DONE; //Done } m_dataLen = 0; m_dataPos = 0; } } } while(len>0); if(!m_dataChunked && (m_dataPos >= m_dataLen)) //All Data has been received { DBG("End of file\n"); m_state = HTTP_DONE; //Done } } void HTTPClient::writeData() //Data has been written & buf is free { if(m_pDataOut == NULL) //Nothing to write (in POST for instance) { m_dataLen = 0; //Reset Data Length m_state = HTTP_READ_HEADERS; return; } int len = m_pDataOut->read(m_buf, CHUNK_SIZE); if( m_dataChunked ) { //Write chunk header char chunkHeader[16]; sprintf(chunkHeader, "%d\r\n", len); int ret = m_pTCPSocket->send(chunkHeader, strlen(chunkHeader)); if(ret < 0)//Error { onResult(HTTP_CONN); return; } } m_pTCPSocket->send(m_buf, len); m_dataPos+=len; if( m_dataChunked ) { m_pTCPSocket->send("\r\n", 2); //Chunk-terminating CRLF } if( ( !m_dataChunked && (m_dataPos >= m_dataLen) ) || ( m_dataChunked && !len ) ) //All Data has been sent { m_dataLen = 0; //Reset Data Length m_state = HTTP_READ_HEADERS; //Wait for resp } } void HTTPClient::onTCPSocketEvent(TCPSocketEvent e) { DBG("Event %d in HTTPClient::onTCPSocketEvent()\n", e); if(m_closed) { DBG("WARN: Discarded\n"); return; } switch(e) { case TCPSOCKET_READABLE: //Incoming data resetTimeout(); switch(m_state) { case HTTP_READ_HEADERS: if( !readHeaders() ) { return; //Connection has been closed or incomplete data } if( m_pDataIn ) { //Data chunked? if(m_respHeaders["Transfer-Encoding"].find("chunked")!=string::npos) { m_dataChunked = true; m_dataPos = 0; m_dataLen = 0; DBG("Encoding is chunked, Content-Type is %s\n", m_respHeaders["Content-Type"].c_str() ); } else { m_dataChunked = false; int len = 0; //DBG("Preparing read... len = %s\n", m_respHeaders["Content-Length"].c_str()); sscanf(m_respHeaders["Content-Length"].c_str(), "%d", &len); m_pDataIn->setDataLen( len ); m_dataPos = 0; m_dataLen = len; DBG("Content-Length is %d, Content-Type is %s\n", len, m_respHeaders["Content-Type"].c_str() ); } m_pDataIn->setDataType( m_respHeaders["Content-Type"] ); } case HTTP_READ_DATA: readData(); break; case HTTP_READ_DATA_INCOMPLETE: break; //We need to handle previously received data first default: //Should not receive data now, req is not complete onResult(HTTP_PRTCL); } //All data has been read, close w/ success :) if( m_state == HTTP_DONE ) { DBG("Done :)!\n"); onResult(HTTP_OK); } break; case TCPSOCKET_CONNECTED: case TCPSOCKET_WRITEABLE: //We can send data resetTimeout(); switch(m_state) { case HTTP_WRITE_HEADERS: //Update headers fields according to m_pDataOut if( m_pDataOut ) { //Data is chunked? if(m_pDataOut->getIsChunked()) { m_dataChunked = true; m_reqHeaders.erase("Content-Length"); m_reqHeaders["Transfer-Encoding"] = "chunked"; } else { m_dataChunked = false; char c_len[16] = "0"; int len = m_pDataOut->getDataLen(); sprintf(c_len, "%d", len); m_dataPos = 0; m_dataLen = len; m_reqHeaders.erase("Transfer-Encoding"); m_reqHeaders["Content-Length"] = string(c_len); } string type = m_pDataOut->getDataType(); if(!type.empty()) { m_reqHeaders["Content-Type"] = type; } else { m_reqHeaders.erase("Content-Type"); } } if( !writeHeaders() ) { return; //Connection has been closed } break; //Wait for writeable event before sending payload case HTTP_WRITE_DATA: writeData(); break; } //Otherwise request has been sent, now wait for resp break; case TCPSOCKET_CONTIMEOUT: case TCPSOCKET_CONRST: case TCPSOCKET_CONABRT: case TCPSOCKET_ERROR: DBG("Connection error.\n"); onResult(HTTP_CONN); case TCPSOCKET_DISCONNECTED: //There might still be some data available for reading //So if we are in a reading state, do not close the socket yet if( (m_state != HTTP_READ_DATA_INCOMPLETE) && (m_state != HTTP_DONE) && (m_state != HTTP_CLOSED) ) { onResult(HTTP_CONN); } DBG("Connection closed by remote host.\n"); break; } } void HTTPClient::onDNSReply(DNSReply r) { if(m_closed) { DBG("WARN: Discarded\n"); return; } if( r != DNS_FOUND ) { DBG("Could not resolve hostname.\n"); onResult(HTTP_DNS); return; } DBG("DNS Resolved to %d.%d.%d.%d.\n",m_server.getIp()[0],m_server.getIp()[1],m_server.getIp()[2],m_server.getIp()[3]); //If no error, m_server has been updated by m_pDnsReq so we're set to go ! m_pDnsReq->close(); delete m_pDnsReq; m_pDnsReq = NULL; connect(); } void HTTPClient::onResult(HTTPResult r) //Called when exchange completed or on failure { if(m_pCbItem && m_pCbMeth) (m_pCbItem->*m_pCbMeth)(r); else if(m_pCb) m_pCb(r); m_blockingResult = r; //Blocking mode close(); //FIXME:Remove suppl. close() calls } void HTTPClient::onTimeout() //Connection has timed out { DBG("Timed out.\n"); onResult(HTTP_TIMEOUT); close(); } //Headers //TODO: Factorize w/ HTTPRequestHandler in a single HTTPHeader class HTTPResult HTTPClient::blockingProcess() //Called in blocking mode, calls Net::poll() until return code is available { //Disable callbacks m_pCb = NULL; m_pCbItem = NULL; m_pCbMeth = NULL; m_blockingResult = HTTP_PROCESSING; do { Net::poll(); } while(m_blockingResult == HTTP_PROCESSING); Net::poll(); //Necessary for cleanup return m_blockingResult; } bool HTTPClient::readHeaders() { static char* line = m_buf; static char key[128]; static char value[128]; if(!m_dataLen) //No incomplete header in buffer, this is the first time we read data { if( readLine(line, 128) > 0 ) { //Check RC m_httpResponseCode = 0; if( sscanf(line, "HTTP/%*d.%*d %d %*[^\r\n]", &m_httpResponseCode) != 1 ) { //Cannot match string, error DBG("Not a correct HTTP answer : %s\n", line); onResult(HTTP_PRTCL); close(); return false; } if(m_httpResponseCode != 200) { DBG("Response: error code %d\n", m_httpResponseCode); HTTPResult res = HTTP_ERROR; switch(m_httpResponseCode) { case 404: res = HTTP_NOTFOUND; break; case 403: res = HTTP_REFUSED; break; default: res = HTTP_ERROR; } onResult(res); close(); return false; } DBG("Response OK\n"); } else { //Empty packet, weird! DBG("Empty packet!\n"); onResult(HTTP_PRTCL); close(); return false; } } bool incomplete = false; while( true ) { int readLen = readLine(line + m_dataLen, 128 - m_dataLen, &incomplete); m_dataLen = 0; if( readLen <= 2 ) //if == 1 or 2, it is an empty line = end of headers { DBG("All headers read.\n"); m_state = HTTP_READ_DATA; break; } else if( incomplete == true ) { m_dataLen = readLen;//Sets data length available in buffer return false; } //DBG("Header : %s\n", line); int n = sscanf(line, "%[^:] : %[^\r\n]", key, value); if ( n == 2 ) { DBG("Read header : %s: %s\n", key, value); m_respHeaders[key] = value; } //TODO: Impl n==1 case (part 2 of previous header) } return true; } bool HTTPClient::writeHeaders() //Called at the first writeData call { static char* line = m_buf; const char* HTTP_METH_STR[] = {"GET", "POST", "HEAD"}; //Req sprintf(line, "%s %s HTTP/1.1\r\nHost: %s\r\n", HTTP_METH_STR[m_meth], m_path.c_str(), m_server.getName()); //Write request m_pTCPSocket->send(line, strlen(line)); DBG("Request: %s\n", line); DBG("Writing headers:\n"); map<string,string>::iterator it; for( it = m_reqHeaders.begin(); it != m_reqHeaders.end(); it++ ) { sprintf(line, "%s: %s\r\n", (*it).first.c_str(), (*it).second.c_str() ); DBG("\r\n%s", line); m_pTCPSocket->send(line, strlen(line)); } m_pTCPSocket->send("\r\n",2); //End of head m_state = HTTP_WRITE_DATA; return true; } int HTTPClient::readLine(char* str, int maxLen, bool* pIncomplete /* = NULL*/) { int ret; int len = 0; if(pIncomplete) *pIncomplete = false; for(int i = 0; i < maxLen - 1; i++) { ret = m_pTCPSocket->recv(str, 1); if(ret != 1) { if(pIncomplete) *pIncomplete = true; break; } if( (len > 1) && *(str-1)=='\r' && *str=='\n' ) { break; } else if( *str=='\n' ) { break; } str++; len++; } *str = 0; return len; }