diff --git a/hardware/arduino/cores/arduino/Print.cpp b/hardware/arduino/cores/arduino/Print.cpp index e541a6ce71d..540dbf70c56 100755 --- a/hardware/arduino/cores/arduino/Print.cpp +++ b/hardware/arduino/cores/arduino/Print.cpp @@ -46,6 +46,10 @@ size_t Print::print(const __FlashStringHelper *ifsh) while (1) { unsigned char c = pgm_read_byte(p++); if (c == 0) break; + if (attachedPrint) + { + attachedPrint->print(c); + } n += write(c); } return n; @@ -55,6 +59,10 @@ size_t Print::print(const String &s) { size_t n = 0; for (uint16_t i = 0; i < s.length(); i++) { + if (attachedPrint) + { + attachedPrint->print(s[i]); + } n += write(s[i]); } return n; @@ -62,11 +70,19 @@ size_t Print::print(const String &s) size_t Print::print(const char str[]) { + if (attachedPrint) + { + attachedPrint->print(str); + } return write(str); } size_t Print::print(char c) { + if (attachedPrint) + { + attachedPrint->print(c); + } return write(c); } @@ -88,6 +104,10 @@ size_t Print::print(unsigned int n, int base) size_t Print::print(long n, int base) { if (base == 0) { + if (attachedPrint) + { + attachedPrint->print(n); + } return write(n); } else if (base == 10) { if (n < 0) { @@ -103,7 +123,14 @@ size_t Print::print(long n, int base) size_t Print::print(unsigned long n, int base) { - if (base == 0) return write(n); + if (base == 0) + { + if (attachedPrint) + { + attachedPrint->print(n); + } + return write(n); + } else return printNumber(n, base); } @@ -219,6 +246,10 @@ size_t Print::printNumber(unsigned long n, uint8_t base) { *--str = c < 10 ? c + '0' : c + 'A' - 10; } while(n); + if (attachedPrint) + { + attachedPrint->print(str); + } return write(str); } diff --git a/hardware/arduino/cores/arduino/Print.h b/hardware/arduino/cores/arduino/Print.h index 1af6b723fc0..684094a29ae 100755 --- a/hardware/arduino/cores/arduino/Print.h +++ b/hardware/arduino/cores/arduino/Print.h @@ -38,9 +38,11 @@ class Print size_t printNumber(unsigned long, uint8_t); size_t printFloat(double, uint8_t); protected: + Print* attachedPrint; // if non-NULL, points to a Print instance where we should output + // any sent/received data void setWriteError(int err = 1) { write_error = err; } public: - Print() : write_error(0) {} + Print() : write_error(0), attachedPrint(NULL) {} int getWriteError() { return write_error; } void clearWriteError() { setWriteError(0); } @@ -49,6 +51,8 @@ class Print size_t write(const char *str) { return write((const uint8_t *)str, strlen(str)); } virtual size_t write(const uint8_t *buffer, size_t size); + void attach(Print& aPrint) { attachedPrint = &aPrint; }; + size_t print(const __FlashStringHelper *); size_t print(const String &); size_t print(const char[]); diff --git a/hardware/arduino/cores/arduino/Stream.cpp b/hardware/arduino/cores/arduino/Stream.cpp index 3d5b9052911..aafb7fcf97d 100644 --- a/hardware/arduino/cores/arduino/Stream.cpp +++ b/hardware/arduino/cores/arduino/Stream.cpp @@ -244,3 +244,27 @@ size_t Stream::readBytesUntil(char terminator, char *buffer, size_t length) return index; // return number of characters, not including null terminator } +String Stream::readString() +{ + String ret; + int c = timedRead(); + while (c >= 0) + { + ret += (char)c; + c = timedRead(); + } + return ret; +} + +String Stream::readStringUntil(char terminator) +{ + String ret; + int c = timedRead(); + while (c >= 0 && c != terminator) + { + ret += (char)c; + c = timedRead(); + } + return ret; +} + diff --git a/hardware/arduino/cores/arduino/Stream.h b/hardware/arduino/cores/arduino/Stream.h index 13f11bee022..58bbf752f33 100644 --- a/hardware/arduino/cores/arduino/Stream.h +++ b/hardware/arduino/cores/arduino/Stream.h @@ -82,6 +82,8 @@ class Stream : public Print // returns the number of characters placed in the buffer (0 means no valid data found) // Arduino String functions to be added here + String readString(); + String readStringUntil(char terminator); protected: long parseInt(char skipChar); // as above but the given skipChar is ignored diff --git a/libraries/Ethernet/EthernetClient.cpp b/libraries/Ethernet/EthernetClient.cpp index a77a62bebc0..8a700f10719 100644 --- a/libraries/Ethernet/EthernetClient.cpp +++ b/libraries/Ethernet/EthernetClient.cpp @@ -97,6 +97,10 @@ int EthernetClient::read() { if ( recv(_sock, &b, 1) > 0 ) { // recv worked + if (attachedPrint) + { + attachedPrint->print((char)b); + } return b; } else @@ -107,7 +111,18 @@ int EthernetClient::read() { } int EthernetClient::read(uint8_t *buf, size_t size) { - return recv(_sock, buf, size); + int ret = recv(_sock, buf, size); + if (ret > 0) + { + if (attachedPrint) + { + for (int i=0; i < ret; i++) + { + attachedPrint->print((char)buf[i]); + } + } + } + return ret; } int EthernetClient::peek() { diff --git a/libraries/HttpClient/HttpClient.cpp b/libraries/HttpClient/HttpClient.cpp new file mode 100644 index 00000000000..6bf8121706e --- /dev/null +++ b/libraries/HttpClient/HttpClient.cpp @@ -0,0 +1,528 @@ +// Class to simplify HTTP fetching on Arduino +// (c) Copyright 2010-2011 MCQN Ltd +// Released under Apache License, version 2.0 + +#include "HttpClient.h" +#include "b64.h" +#include +#include +#include + +// Initialize constants +const char* HttpClient::kUserAgent = "Arduino/2.0"; +const char* HttpClient::kGet = "GET"; +const char* HttpClient::kPost = "POST"; +const char* HttpClient::kPut = "PUT"; +const char* HttpClient::kDelete = "DELETE"; +const char* HttpClient::kContentLengthPrefix = "Content-Length: "; + +HttpClient::HttpClient(Client& aClient, const char* aProxy, uint16_t aProxyPort) + : iClient(&aClient), iProxyPort(aProxyPort) +{ + resetState(); + if (aProxy) + { + // Resolve the IP address for the proxy + DNSClient dns; + dns.begin(Ethernet.dnsServerIP()); + // Not ideal that we discard any errors here, but not a lot we can do in the ctor + // and we'll get a connect error later anyway + (void)dns.getHostByName(aProxy, iProxyAddress); + } +} + +void HttpClient::resetState() +{ + iState = eIdle; + iStatusCode = 0; + iContentLength = 0; + iBodyLengthConsumed = 0; + iContentLengthPtr = 0; +} + +void HttpClient::stop() +{ + iClient->stop(); + resetState(); +} + +void HttpClient::beginRequest() +{ + iState = eRequestStarted; +} + +int HttpClient::startRequest(const char* aServerName, uint16_t aServerPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) +{ + tHttpState initialState = iState; + if ((eIdle != iState) && (eRequestStarted != iState)) + { + return HTTP_ERROR_API; + } + + if (iProxyPort) + { + if (!iClient->connect(iProxyAddress, iProxyPort) > 0) + { +#ifdef LOGGING + Serial.println("Proxy connection failed"); +#endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + else + { + if (!iClient->connect(aServerName, aServerPort) > 0) + { +#ifdef LOGGING + Serial.println("Connection failed"); +#endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + + // Now we're connected, send the first part of the request + int ret = sendInitialHeaders(aServerName, IPAddress(0,0,0,0), aServerPort, aURLPath, aHttpMethod, aUserAgent); + if ((initialState == eIdle) && (HTTP_SUCCESS == ret)) + { + // This was a simple version of the API, so terminate the headers now + finishHeaders(); + } + // else we'll call it in endRequest or in the first call to print, etc. + + return ret; +} + +int HttpClient::startRequest(const IPAddress& aServerAddress, const char* aServerName, uint16_t aServerPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) +{ + tHttpState initialState = iState; + if ((eIdle != iState) && (eRequestStarted != iState)) + { + return HTTP_ERROR_API; + } + + if (iProxyPort) + { + if (!iClient->connect(iProxyAddress, iProxyPort) > 0) + { +#ifdef LOGGING + Serial.println("Proxy connection failed"); +#endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + else + { + if (!iClient->connect(aServerAddress, aServerPort) > 0) + { +#ifdef LOGGING + Serial.println("Connection failed"); +#endif + return HTTP_ERROR_CONNECTION_FAILED; + } + } + + // Now we're connected, send the first part of the request + int ret = sendInitialHeaders(aServerName, aServerAddress, aServerPort, aURLPath, aHttpMethod, aUserAgent); + if ((initialState == eIdle) && (HTTP_SUCCESS == ret)) + { + // This was a simple version of the API, so terminate the headers now + finishHeaders(); + } + // else we'll call it in endRequest or in the first call to print, etc. + + return ret; +} + +int HttpClient::sendInitialHeaders(const char* aServerName, IPAddress aServerIP, uint16_t aPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) +{ +#ifdef LOGGING + Serial.println("Connected"); +#endif + // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" + iClient->print(aHttpMethod); + iClient->print(" "); + if (iProxyPort) + { + // We're going through a proxy, send a full URL + iClient->print("http://"); + if (aServerName) + { + // We've got a server name, so use it + iClient->print(aServerName); + } + else + { + // We'll have to use the IP address + iClient->print(aServerIP); + } + if (aPort != kHttpPort) + { + iClient->print(":"); + iClient->print(aPort); + } + } + iClient->print(aURLPath); + iClient->println(" HTTP/1.0"); + // The host header, if required + if (aServerName) + { + sendHeader("Host", aServerName); + } + // And user-agent string + iClient->print("User-Agent: "); + if (aUserAgent) + { + iClient->println(aUserAgent); + } + else + { + iClient->println(kUserAgent); + } + + // Everything has gone well + iState = eRequestStarted; + return HTTP_SUCCESS; +} + +void HttpClient::sendHeader(const char* aHeader) +{ + iClient->println(aHeader); +} + +void HttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +void HttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) +{ + iClient->print(aHeaderName); + iClient->print(": "); + iClient->println(aHeaderValue); +} + +void HttpClient::sendBasicAuth(const char* aUser, const char* aPassword) +{ + // Send the initial part of this header line + iClient->print("Authorization: Basic "); + // Now Base64 encode "aUser:aPassword" and send that + // This seems trickier than it should be but it's mostly to avoid either + // (a) some arbitrarily sized buffer which hopes to be big enough, or + // (b) allocating and freeing memory + // ...so we'll loop through 3 bytes at a time, outputting the results as we + // go. + // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data + unsigned char input[3]; + unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print + int userLen = strlen(aUser); + int passwordLen = strlen(aPassword); + int inputOffset = 0; + for (int i = 0; i < (userLen+1+passwordLen); i++) + { + // Copy the relevant input byte into the input + if (i < userLen) + { + input[inputOffset++] = aUser[i]; + } + else if (i == userLen) + { + input[inputOffset++] = ':'; + } + else + { + input[inputOffset++] = aPassword[i-(userLen+1)]; + } + // See if we've got a chunk to encode + if ( (inputOffset == 3) || (i == userLen+passwordLen) ) + { + // We've either got to a 3-byte boundary, or we've reached then end + b64_encode(input, inputOffset, output, 4); + // NUL-terminate the output string + output[4] = '\0'; + // And write it out + iClient->print((char*)output); +// FIXME We might want to fill output with '=' characters if b64_encode doesn't +// FIXME do it for us when we're encoding the final chunk + inputOffset = 0; + } + } + // And end the header we've sent + iClient->println(); +} + +void HttpClient::finishHeaders() +{ + iClient->println(); + iState = eRequestSent; +} + +void HttpClient::endRequest() +{ + if (iState < eRequestSent) + { + // We still need to finish off the headers + finishHeaders(); + } + // else the end of headers has already been sent, so nothing to do here +} + +int HttpClient::responseStatusCode() +{ + if (iState < eRequestSent) + { + return HTTP_ERROR_API; + } + // The first line will be of the form Status-Line: + // HTTP-Version SP Status-Code SP Reason-Phrase CRLF + // Where HTTP-Version is of the form: + // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT + + char c = '\0'; + do + { + // Make sure the status code is reset, and likewise the state. This + // lets us easily cope with 1xx informational responses by just + // ignoring them really, and reading the next line for a proper response + iStatusCode = 0; + iState = eRequestSent; + + unsigned long timeoutStart = millis(); + // Psuedo-regexp we're expecting before the status-code + const char* statusPrefix = "HTTP/*.* "; + const char* statusPtr = statusPrefix; + // Whilst we haven't timed out & haven't reached the end of the headers + while ((c != '\n') && + ( (millis() - timeoutStart) < kHttpResponseTimeout )) + { + if (available()) + { + c = read(); + switch(iState) + { + case eRequestSent: + // We haven't reached the status code yet + if ( (*statusPtr == '*') || (*statusPtr == c) ) + { + // This character matches, just move along + statusPtr++; + if (*statusPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingStatusCode; + } + } + else + { + return HTTP_ERROR_INVALID_RESPONSE; + } + break; + case eReadingStatusCode: + if (isdigit(c)) + { + // This assumes we won't get more than the 3 digits we + // want + iStatusCode = iStatusCode*10 + (c - '0'); + } + else + { + // We've reached the end of the status code + // We could sanity check it here or double-check for ' ' + // rather than anything else, but let's be lenient + iState = eStatusCodeRead; + } + break; + case eStatusCodeRead: + // We're just waiting for the end of the line now + break; + }; + // We read something, reset the timeout counter + timeoutStart = millis(); + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(kHttpWaitForDataDelay); + } + } + if ( (c == '\n') && (iStatusCode < 200) ) + { + // We've reached the end of an informational status line + c = '\0'; // Clear c so we'll go back into the data reading loop + } + } + // If we've read a status code successfully but it's informational (1xx) + // loop back to the start + while ( (iState == eStatusCodeRead) && (iStatusCode < 200) ); + + if ( (c == '\n') && (iState == eStatusCodeRead) ) + { + // We've read the status-line successfully + return iStatusCode; + } + else if (c != '\n') + { + // We must've timed out before we reached the end of the line + return HTTP_ERROR_TIMED_OUT; + } + else + { + // This wasn't a properly formed status line, or at least not one we + // could understand + return HTTP_ERROR_INVALID_RESPONSE; + } +} + +int HttpClient::skipResponseHeaders() +{ + // Just keep reading until we finish reading the headers or time out + unsigned long timeoutStart = millis(); + // Whilst we haven't timed out & haven't reached the end of the headers + while ((!endOfHeadersReached()) && + ( (millis() - timeoutStart) < kHttpResponseTimeout )) + { + if (available()) + { + (void)readHeader(); + // We read something, reset the timeout counter + timeoutStart = millis(); + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(kHttpWaitForDataDelay); + } + } + if (endOfHeadersReached()) + { + // Success + return HTTP_SUCCESS; + } + else + { + // We must've timed out + return HTTP_ERROR_TIMED_OUT; + } +} + +bool HttpClient::endOfBodyReached() +{ + if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) + { + // We've got to the body and we know how long it will be + return (iBodyLengthConsumed >= contentLength()); + } + return false; +} + +int HttpClient::read() +{ + uint8_t b[1]; + int ret = read(b, 1); + if (ret == 1) + { + return b[0]; + } + else + { + return -1; + } +} + +int HttpClient::read(uint8_t *buf, size_t size) +{ + int ret =iClient->read(buf, size); + if (endOfHeadersReached() && iContentLength > 0) + { + // We're outputting the body now and we've seen a Content-Length header + // So keep track of how many bytes are left + if (ret >= 0) + { + iBodyLengthConsumed += ret; + } + } + return ret; +} + +int HttpClient::readHeader() +{ + char c = read(); + + if (endOfHeadersReached()) + { + // We've passed the headers, but rather than return an error, we'll just + // act as a slightly less efficient version of read() + return c; + } + + // Whilst reading out the headers to whoever wants them, we'll keep an + // eye out for the "Content-Length" header + switch(iState) + { + case eStatusCodeRead: + // We're at the start of a line, or somewhere in the middle of reading + // the Content-Length prefix + if (*iContentLengthPtr == c) + { + // This character matches, just move along + iContentLengthPtr++; + if (*iContentLengthPtr == '\0') + { + // We've reached the end of the prefix + iState = eReadingContentLength; + // Just in case we get multiple Content-Length headers, this + // will ensure we just get the value of the last one + iContentLength = 0; + } + } + else if ((iContentLengthPtr == kContentLengthPrefix) && (c == '\r')) + { + // We've found a '\r' at the start of a line, so this is probably + // the end of the headers + iState = eLineStartingCRFound; + } + else + { + // This isn't the Content-Length header, skip to the end of the line + iState = eSkipToEndOfHeader; + } + break; + case eReadingContentLength: + if (isdigit(c)) + { + iContentLength = iContentLength*10 + (c - '0'); + } + else + { + // We've reached the end of the content length + // We could sanity check it here or double-check for "\r\n" + // rather than anything else, but let's be lenient + iState = eSkipToEndOfHeader; + } + break; + case eLineStartingCRFound: + if (c == '\n') + { + iState = eReadingBody; + } + break; + default: + // We're just waiting for the end of the line now + break; + }; + + if ( (c == '\n') && !endOfHeadersReached() ) + { + // We've got to the end of this line, start processing again + iState = eStatusCodeRead; + iContentLengthPtr = kContentLengthPrefix; + } + // And return the character read to whoever wants it + return c; +} + + + diff --git a/libraries/HttpClient/HttpClient.h b/libraries/HttpClient/HttpClient.h new file mode 100644 index 00000000000..78cbefe957c --- /dev/null +++ b/libraries/HttpClient/HttpClient.h @@ -0,0 +1,436 @@ +// Class to simplify HTTP fetching on Arduino +// (c) Copyright MCQN Ltd. 2010-2012 +// Released under Apache License, version 2.0 + +#ifndef HttpClient_h +#define HttpClient_h + +#include +#include +#include "Ethernet.h" +#include "Client.h" + +static const int HTTP_SUCCESS =0; +// The end of the headers has been reached. This consumes the '\n' +// Could not connect to the server +static const int HTTP_ERROR_CONNECTION_FAILED =-1; +// This call was made when the HttpClient class wasn't expecting it +// to be called. Usually indicates your code is using the class +// incorrectly +static const int HTTP_ERROR_API =-2; +// Spent too long waiting for a reply +static const int HTTP_ERROR_TIMED_OUT =-3; +// The response from the server is invalid, is it definitely an HTTP +// server? +static const int HTTP_ERROR_INVALID_RESPONSE =-4; + +class HttpClient : public Client +{ +public: + static const int kNoContentLengthHeader =-1; + static const int kHttpPort =80; + static const char* kUserAgent; + static const char* kGet; + static const char* kPost; + static const char* kPut; + static const char* kDelete; + +// FIXME Write longer API request, using port and user-agent, example +// FIXME Update tempToPachube example to calculate Content-Length correctly + + HttpClient(Client& aClient, const char* aProxy =NULL, uint16_t aProxyPort =0); + + /** Start a more complex request. + Use this when you need to send additional headers in the request, + but you will also need to call endRequest() when you are finished. + */ + void beginRequest(); + + /** End a more complex request. + Use this when you need to have sent additional headers in the request, + but you will also need to call beginRequest() at the start. + */ + void endRequest(); + + /** Connect to the server and start to send a GET request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int get(const char* aServerName, uint16_t aServerPort, const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerName, aServerPort, aURLPath, kGet, aUserAgent); } + + /** Connect to the server and start to send a GET request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int get(const char* aServerName, const char* aURLPath, const char* aUserAgent =NULL) + { return startRequest(aServerName, kHttpPort, aURLPath, kGet, aUserAgent); } + + /** Connect to the server and start to send a GET request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int get(const IPAddress& aServerAddress, + const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, kGet, aUserAgent); } + + /** Connect to the server and start to send a GET request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int get(const IPAddress& aServerAddress, + const char* aServerName, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, kGet, aUserAgent); } + + /** Connect to the server and start to send a POST request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int post(const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerName, aServerPort, aURLPath, kPost, aUserAgent); } + + /** Connect to the server and start to send a POST request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int post(const char* aServerName, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerName, kHttpPort, aURLPath, kPost, aUserAgent); } + + /** Connect to the server and start to send a POST request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int post(const IPAddress& aServerAddress, + const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, kPost, aUserAgent); } + + /** Connect to the server and start to send a POST request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int post(const IPAddress& aServerAddress, + const char* aServerName, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, kPost, aUserAgent); } + + /** Connect to the server and start to send a PUT request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int put(const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerName, aServerPort, aURLPath, kPut, aUserAgent); } + + /** Connect to the server and start to send a PUT request. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int put(const char* aServerName, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerName, kHttpPort, aURLPath, kPut, aUserAgent); } + + /** Connect to the server and start to send a PUT request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int put(const IPAddress& aServerAddress, + const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, kPut, aUserAgent); } + + /** Connect to the server and start to send a PUT request. This version connects + doesn't perform a DNS lookup and just connects to the given IP address. + @param aServerAddress IP address of the server to connect to + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aURLPath Url to request + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int put(const IPAddress& aServerAddress, + const char* aServerName, + const char* aURLPath, + const char* aUserAgent =NULL) + { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, kPut, aUserAgent); } + + /** Connect to the server and start to send the request. + @param aServerName Name of the server being connected to. + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int startRequest(const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aHttpMethod, + const char* aUserAgent); + + /** Connect to the server and start to send the request. + @param aServerAddress IP address of the server to connect to. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerPort Port to connect to on the server + @param aURLPath Url to request + @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int startRequest(const IPAddress& aServerAddress, + const char* aServerName, + uint16_t aServerPort, + const char* aURLPath, + const char* aHttpMethod, + const char* aUserAgent); + + /** Send an additional header line. This can only be called in between the + calls to startRequest and finishRequest. + @param aHeader Header line to send, in its entirety (but without the + trailing CRLF. E.g. "Authorization: Basic YQDDCAIGES" + */ + void sendHeader(const char* aHeader); + + /** Send an additional header line. This is an alternate form of + sendHeader() which takes the header name and content as separate strings. + The call will add the ": " to separate the header, so for example, to + send a XXXXXX header call sendHeader("XXXXX", "Something") + @param aHeaderName Type of header being sent + @param aHeaderValue Value for that header + */ + void sendHeader(const char* aHeaderName, const char* aHeaderValue); + + /** Send an additional header line. This is an alternate form of + sendHeader() which takes the header name and content separately but where + the value is provided as an integer. + The call will add the ": " to separate the header, so for example, to + send a XXXXXX header call sendHeader("XXXXX", 123) + @param aHeaderName Type of header being sent + @param aHeaderValue Value for that header + */ + void sendHeader(const char* aHeaderName, const int aHeaderValue); + + /** Send a basic authentication header. This will encode the given username + and password, and send them in suitable header line for doing Basic + Authentication. + @param aUser Username for the authorization + @param aPassword Password for the user aUser + */ + void sendBasicAuth(const char* aUser, const char* aPassword); + + /** Finish sending the HTTP request. This basically just sends the blank + line to signify the end of the request + */ + void finishRequest(); + + /** Get the HTTP status code contained in the response. + For example, 200 for successful request, 404 for file not found, etc. + */ + int responseStatusCode(); + + /** Read the next character of the response headers. + This functions in the same way as read() but to be used when reading + through the headers. Check whether or not the end of the headers has + been reached by calling endOfHeadersReached(), although after that point + this will still return data as read() would, but slightly less efficiently + @return The next character of the response headers + */ + int readHeader(); + + /** Skip any response headers to get to the body. + Use this if you don't want to do any special processing of the headers + returned in the response. You can also use it after you've found all of + the headers you're interested in, and just want to get on with processing + the body. + @return HTTP_SUCCESS if successful, else an error code + */ + int skipResponseHeaders(); + + /** Test whether all of the response headers have been consumed. + @return true if we are now processing the response body, else false + */ + bool endOfHeadersReached() { return (iState == eReadingBody); }; + + /** Test whether the end of the body has been reached. + Only works if the Content-Length header was returned by the server + @return true if we are now at the end of the body, else false + */ + bool endOfBodyReached(); + virtual bool endOfStream() { return endOfBodyReached(); }; + virtual bool completed() { return endOfBodyReached(); }; + + /** Return the length of the body. + @return Length of the body, in bytes, or kNoContentLengthHeader if no + Content-Length header was returned by the server + */ + int contentLength() { return iContentLength; }; + + // Inherited from Print + // Note: 1st call to these indicates the user is sending the body, so if need + // Note: be we should finish the header first + virtual size_t write(uint8_t aByte) { if (iState < eRequestSent) { finishHeaders(); }; return iClient-> write(aByte); }; + virtual size_t write(const uint8_t *aBuffer, size_t aSize) { if (iState < eRequestSent) { finishHeaders(); }; return iClient->write(aBuffer, aSize); }; + // Inherited from Stream + virtual int available() { return iClient->available(); }; + /** Read the next byte from the server. + @return Byte read or -1 if there are no bytes available. + */ + virtual int read(); + virtual int read(uint8_t *buf, size_t size); + virtual int peek() { return iClient->peek(); }; + virtual void flush() { return iClient->flush(); }; + + // Inherited from Client + virtual int connect(IPAddress ip, uint16_t port) { return iClient->connect(ip, port); }; + virtual int connect(const char *host, uint16_t port) { return iClient->connect(host, port); }; + virtual void stop(); + virtual uint8_t connected() { iClient->connected(); }; + virtual operator bool() { return bool(iClient); }; +protected: + /** Reset internal state data back to the "just initialised" state + */ + void resetState(); + + /** Send the first part of the request and the initial headers. + @param aServerName Name of the server being connected to. If NULL, the + "Host" header line won't be sent + @param aServerIP IP address of the server (only used if we're going through a + proxy and aServerName is NULL + @param aServerPort Port of the server being connected to. + @param aURLPath Url to request + @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. + @param aUserAgent User-Agent string to send. If NULL the default + user-agent kUserAgent will be sent + @return 0 if successful, else error + */ + int sendInitialHeaders(const char* aServerName, + IPAddress aServerIP, + uint16_t aPort, + const char* aURLPath, + const char* aHttpMethod, + const char* aUserAgent); + + /* Let the server know that we've reached the end of the headers + */ + void finishHeaders(); + + // Number of milliseconds that we wait each time there isn't any data + // available to be read (during status code and header processing) + static const int kHttpWaitForDataDelay = 1000; + // Number of milliseconds that we'll wait in total without receiveing any + // data before returning HTTP_ERROR_TIMED_OUT (during status code and header + // processing) + static const int kHttpResponseTimeout = 30*1000; + static const char* kContentLengthPrefix; + typedef enum { + eIdle, + eRequestStarted, + eRequestSent, + eReadingStatusCode, + eStatusCodeRead, + eReadingContentLength, + eSkipToEndOfHeader, + eLineStartingCRFound, + eReadingBody + } tHttpState; + // Ethernet client we're using + Client* iClient; + // Current state of the finite-state-machine + tHttpState iState; + // Stores the status code for the response, once known + int iStatusCode; + // Stores the value of the Content-Length header, if present + int iContentLength; + // How many bytes of the response body have been read by the user + int iBodyLengthConsumed; + // How far through a Content-Length header prefix we are + const char* iContentLengthPtr; + // Address of the proxy to use, if we're using one + IPAddress iProxyAddress; + uint16_t iProxyPort; +}; + +#endif diff --git a/libraries/HttpClient/b64.cpp b/libraries/HttpClient/b64.cpp new file mode 100644 index 00000000000..b926cada78a --- /dev/null +++ b/libraries/HttpClient/b64.cpp @@ -0,0 +1,70 @@ +// Simple Base64 code +// (c) Copyright 2010 MCQN Ltd. +// Released under Apache License, version 2.0 + +#include "b64.h" + +/* Simple test program +#include +void main() +{ + char* in = "amcewen"; + char out[22]; + + b64_encode(in, 15, out, 22); + out[21] = '\0'; + + printf(out); +} +*/ + +int b64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen) +{ + // Work out if we've got enough space to encode the input + // Every 6 bits of input becomes a byte of output + if (aOutputLen < (aInputLen*8)/6) + { + // FIXME Should we return an error here, or just the length + return (aInputLen*8)/6; + } + + // If we get here we've got enough space to do the encoding + + const char* b64_dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (aInputLen == 3) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4|(aInput[1]>>4)]; + aOutput[2] = b64_dictionary[(aInput[1]&0x0F)<<2|(aInput[2]>>6)]; + aOutput[3] = b64_dictionary[aInput[2]&0x3F]; + } + else if (aInputLen == 2) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4|(aInput[1]>>4)]; + aOutput[2] = b64_dictionary[(aInput[1]&0x0F)<<2]; + aOutput[3] = '='; + } + else if (aInputLen == 1) + { + aOutput[0] = b64_dictionary[aInput[0] >> 2]; + aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4]; + aOutput[2] = '='; + aOutput[3] = '='; + } + else + { + // Break the input into 3-byte chunks and process each of them + int i; + for (i = 0; i < aInputLen/3; i++) + { + b64_encode(&aInput[i*3], 3, &aOutput[i*4], 4); + } + if (aInputLen % 3 > 0) + { + // It doesn't fit neatly into a 3-byte chunk, so process what's left + b64_encode(&aInput[i*3], aInputLen % 3, &aOutput[i*4], aOutputLen - (i*4)); + } + } +} + diff --git a/libraries/HttpClient/b64.h b/libraries/HttpClient/b64.h new file mode 100644 index 00000000000..cdb1226a93d --- /dev/null +++ b/libraries/HttpClient/b64.h @@ -0,0 +1,6 @@ +#ifndef b64_h +#define b64_h + +int b64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen); + +#endif diff --git a/libraries/HttpClient/examples/SimpleHttpExample/SimpleHttpExample.ino b/libraries/HttpClient/examples/SimpleHttpExample/SimpleHttpExample.ino new file mode 100644 index 00000000000..f12f987fca5 --- /dev/null +++ b/libraries/HttpClient/examples/SimpleHttpExample/SimpleHttpExample.ino @@ -0,0 +1,119 @@ +// (c) Copyright 2010-2012 MCQN Ltd. +// Released under Apache License, version 2.0 +// +// Simple example to show how to use the HttpClient library +// Get's the web page given at http:// and +// outputs the content to the serial port + +#include +#include +#include +#include + +// This example downloads the URL "http://arduino.cc/" + +// Name of the server we want to connect to +const char kHostname[] = "arduino.cc"; +// Path to download (this is the bit after the hostname in the URL +// that you want to download +const char kPath[] = "/"; + +byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; + +// Number of milliseconds to wait without receiving any data before we give up +const int kNetworkTimeout = 30*1000; +// Number of milliseconds to wait if no data is available before trying again +const int kNetworkDelay = 1000; + +void setup() +{ + // initialize serial communications at 9600 bps: + Serial.begin(9600); + + while (Ethernet.begin(mac) != 1) + { + Serial.println("Error getting IP address via DHCP, trying again..."); + delay(15000); + } +} + +void loop() +{ + int err =0; + + EthernetClient c; + HttpClient http(c); + + err = http.get(kHostname, kPath); + if (err == 0) + { + Serial.println("startedRequest ok"); + + err = http.responseStatusCode(); + if (err >= 0) + { + Serial.print("Got status code: "); + Serial.println(err); + + // Usually you'd check that the response code is 200 or a + // similar "success" code (200-299) before carrying on, + // but we'll print out whatever response we get + + err = http.skipResponseHeaders(); + if (err >= 0) + { + int bodyLen = http.contentLength(); + Serial.print("Content length is: "); + Serial.println(bodyLen); + Serial.println(); + Serial.println("Body returned follows:"); + + // Now we've got to the body, so we can print it out + unsigned long timeoutStart = millis(); + char c; + // Whilst we haven't timed out & haven't reached the end of the body + while ( (http.connected() || http.available()) && + ((millis() - timeoutStart) < kNetworkTimeout) ) + { + if (http.available()) + { + c = http.read(); + // Print out this character + Serial.print(c); + + bodyLen--; + // We read something, reset the timeout counter + timeoutStart = millis(); + } + else + { + // We haven't got any data, so let's pause to allow some to + // arrive + delay(kNetworkDelay); + } + } + } + else + { + Serial.print("Failed to skip response headers: "); + Serial.println(err); + } + } + else + { + Serial.print("Getting response failed: "); + Serial.println(err); + } + } + else + { + Serial.print("Connect failed: "); + Serial.println(err); + } + http.stop(); + + // And just stop, now that we've tried a download + while(1); +} + + diff --git a/libraries/HttpClient/keywords.txt b/libraries/HttpClient/keywords.txt new file mode 100644 index 00000000000..cdefda4309e --- /dev/null +++ b/libraries/HttpClient/keywords.txt @@ -0,0 +1,39 @@ +####################################### +# Syntax Coloring Map For HttpClient +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +HttpClient KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +get KEYWORD2 +post KEYWORD2 +put KEYWORD2 +startRequest KEYWORD2 +beginRequest KEYWORD2 +sendHeader KEYWORD2 +sendBasicAuth KEYWORD2 +endRequest KEYWORD2 +responseStatusCode KEYWORD2 +readHeader KEYWORD2 +skipResponseHeaders KEYWORD2 +endOfHeadersReached KEYWORD2 +endOfBodyReached KEYWORD2 +completed KEYWORD2 +contentLength KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### +HTTP_SUCCESS LITERAL1 +HTTP_ERROR_CONNECTION_FAILED LITERAL1 +HTTP_ERROR_API LITERAL1 +HTTP_ERROR_TIMED_OUT LITERAL1 +HTTP_ERROR_INVALID_RESPONSE LITERAL1 +