├── HttpClient.cpp ├── HttpClient.h ├── README.md ├── b64.cpp ├── b64.h ├── examples └── SimpleHttpExample │ └── SimpleHttpExample.ino ├── keywords.txt └── library.properties /HttpClient.cpp: -------------------------------------------------------------------------------- 1 | // Class to simplify HTTP fetching on Arduino 2 | // (c) Copyright 2010-2011 MCQN Ltd 3 | // Released under Apache License, version 2.0 4 | 5 | #include "HttpClient.h" 6 | #include "b64.h" 7 | #ifdef PROXY_ENABLED // currently disabled as introduces dependency on Dns.h in Ethernet 8 | #include 9 | #endif 10 | 11 | // Initialize constants 12 | const char* HttpClient::kUserAgent = "Arduino/2.2.0"; 13 | const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": "; 14 | 15 | #ifdef PROXY_ENABLED // currently disabled as introduces dependency on Dns.h in Ethernet 16 | HttpClient::HttpClient(Client& aClient, const char* aProxy, uint16_t aProxyPort) 17 | : iClient(&aClient), iProxyPort(aProxyPort) 18 | { 19 | resetState(); 20 | if (aProxy) 21 | { 22 | // Resolve the IP address for the proxy 23 | DNSClient dns; 24 | dns.begin(Ethernet.dnsServerIP()); 25 | // Not ideal that we discard any errors here, but not a lot we can do in the ctor 26 | // and we'll get a connect error later anyway 27 | (void)dns.getHostByName(aProxy, iProxyAddress); 28 | } 29 | } 30 | #else 31 | HttpClient::HttpClient(Client& aClient) 32 | : iClient(&aClient), iProxyPort(0) 33 | { 34 | resetState(); 35 | } 36 | #endif 37 | 38 | void HttpClient::resetState() 39 | { 40 | iState = eIdle; 41 | iStatusCode = 0; 42 | iContentLength = 0; 43 | iBodyLengthConsumed = 0; 44 | iContentLengthPtr = kContentLengthPrefix; 45 | iHttpResponseTimeout = kHttpResponseTimeout; 46 | } 47 | 48 | void HttpClient::stop() 49 | { 50 | iClient->stop(); 51 | resetState(); 52 | } 53 | 54 | void HttpClient::beginRequest() 55 | { 56 | iState = eRequestStarted; 57 | } 58 | 59 | int HttpClient::startRequest(const char* aServerName, uint16_t aServerPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) 60 | { 61 | tHttpState initialState = iState; 62 | if ((eIdle != iState) && (eRequestStarted != iState)) 63 | { 64 | return HTTP_ERROR_API; 65 | } 66 | 67 | #ifdef PROXY_ENABLED 68 | if (iProxyPort) 69 | { 70 | if (!iClient->connect(iProxyAddress, iProxyPort) > 0) 71 | { 72 | #ifdef LOGGING 73 | Serial.println("Proxy connection failed"); 74 | #endif 75 | return HTTP_ERROR_CONNECTION_FAILED; 76 | } 77 | } 78 | else 79 | #endif 80 | { 81 | if (!iClient->connect(aServerName, aServerPort) > 0) 82 | { 83 | #ifdef LOGGING 84 | Serial.println("Connection failed"); 85 | #endif 86 | return HTTP_ERROR_CONNECTION_FAILED; 87 | } 88 | } 89 | 90 | // Now we're connected, send the first part of the request 91 | int ret = sendInitialHeaders(aServerName, IPAddress(0,0,0,0), aServerPort, aURLPath, aHttpMethod, aUserAgent); 92 | if ((initialState == eIdle) && (HTTP_SUCCESS == ret)) 93 | { 94 | // This was a simple version of the API, so terminate the headers now 95 | finishHeaders(); 96 | } 97 | // else we'll call it in endRequest or in the first call to print, etc. 98 | 99 | return ret; 100 | } 101 | 102 | int HttpClient::startRequest(const IPAddress& aServerAddress, const char* aServerName, uint16_t aServerPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) 103 | { 104 | tHttpState initialState = iState; 105 | if ((eIdle != iState) && (eRequestStarted != iState)) 106 | { 107 | return HTTP_ERROR_API; 108 | } 109 | 110 | #ifdef PROXY_ENABLED 111 | if (iProxyPort) 112 | { 113 | if (!iClient->connect(iProxyAddress, iProxyPort) > 0) 114 | { 115 | #ifdef LOGGING 116 | Serial.println("Proxy connection failed"); 117 | #endif 118 | return HTTP_ERROR_CONNECTION_FAILED; 119 | } 120 | } 121 | else 122 | #endif 123 | { 124 | if (!iClient->connect(aServerAddress, aServerPort) > 0) 125 | { 126 | #ifdef LOGGING 127 | Serial.println("Connection failed"); 128 | #endif 129 | return HTTP_ERROR_CONNECTION_FAILED; 130 | } 131 | } 132 | 133 | // Now we're connected, send the first part of the request 134 | int ret = sendInitialHeaders(aServerName, aServerAddress, aServerPort, aURLPath, aHttpMethod, aUserAgent); 135 | if ((initialState == eIdle) && (HTTP_SUCCESS == ret)) 136 | { 137 | // This was a simple version of the API, so terminate the headers now 138 | finishHeaders(); 139 | } 140 | // else we'll call it in endRequest or in the first call to print, etc. 141 | 142 | return ret; 143 | } 144 | 145 | int HttpClient::sendInitialHeaders(const char* aServerName, IPAddress aServerIP, uint16_t aPort, const char* aURLPath, const char* aHttpMethod, const char* aUserAgent) 146 | { 147 | #ifdef LOGGING 148 | Serial.println("Connected"); 149 | #endif 150 | // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" 151 | iClient->print(aHttpMethod); 152 | iClient->print(" "); 153 | #ifdef PROXY_ENABLED 154 | if (iProxyPort) 155 | { 156 | // We're going through a proxy, send a full URL 157 | iClient->print("http://"); 158 | if (aServerName) 159 | { 160 | // We've got a server name, so use it 161 | iClient->print(aServerName); 162 | } 163 | else 164 | { 165 | // We'll have to use the IP address 166 | iClient->print(aServerIP); 167 | } 168 | if (aPort != kHttpPort) 169 | { 170 | iClient->print(":"); 171 | iClient->print(aPort); 172 | } 173 | } 174 | #endif 175 | iClient->print(aURLPath); 176 | iClient->println(" HTTP/1.1"); 177 | // The host header, if required 178 | if (aServerName) 179 | { 180 | iClient->print("Host: "); 181 | iClient->print(aServerName); 182 | if (aPort != kHttpPort) 183 | { 184 | iClient->print(":"); 185 | iClient->print(aPort); 186 | } 187 | iClient->println(); 188 | } 189 | // And user-agent string 190 | if (aUserAgent) 191 | { 192 | sendHeader(HTTP_HEADER_USER_AGENT, aUserAgent); 193 | } 194 | else 195 | { 196 | sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent); 197 | } 198 | // We don't support persistent connections, so tell the server to 199 | // close this connection after we're done 200 | sendHeader(HTTP_HEADER_CONNECTION, "close"); 201 | 202 | // Everything has gone well 203 | iState = eRequestStarted; 204 | return HTTP_SUCCESS; 205 | } 206 | 207 | void HttpClient::sendHeader(const char* aHeader) 208 | { 209 | iClient->println(aHeader); 210 | } 211 | 212 | void HttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) 213 | { 214 | iClient->print(aHeaderName); 215 | iClient->print(": "); 216 | iClient->println(aHeaderValue); 217 | } 218 | 219 | void HttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) 220 | { 221 | iClient->print(aHeaderName); 222 | iClient->print(": "); 223 | iClient->println(aHeaderValue); 224 | } 225 | 226 | void HttpClient::sendBasicAuth(const char* aUser, const char* aPassword) 227 | { 228 | // Send the initial part of this header line 229 | iClient->print("Authorization: Basic "); 230 | // Now Base64 encode "aUser:aPassword" and send that 231 | // This seems trickier than it should be but it's mostly to avoid either 232 | // (a) some arbitrarily sized buffer which hopes to be big enough, or 233 | // (b) allocating and freeing memory 234 | // ...so we'll loop through 3 bytes at a time, outputting the results as we 235 | // go. 236 | // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data 237 | unsigned char input[3]; 238 | unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print 239 | int userLen = strlen(aUser); 240 | int passwordLen = strlen(aPassword); 241 | int inputOffset = 0; 242 | for (int i = 0; i < (userLen+1+passwordLen); i++) 243 | { 244 | // Copy the relevant input byte into the input 245 | if (i < userLen) 246 | { 247 | input[inputOffset++] = aUser[i]; 248 | } 249 | else if (i == userLen) 250 | { 251 | input[inputOffset++] = ':'; 252 | } 253 | else 254 | { 255 | input[inputOffset++] = aPassword[i-(userLen+1)]; 256 | } 257 | // See if we've got a chunk to encode 258 | if ( (inputOffset == 3) || (i == userLen+passwordLen) ) 259 | { 260 | // We've either got to a 3-byte boundary, or we've reached then end 261 | b64_encode(input, inputOffset, output, 4); 262 | // NUL-terminate the output string 263 | output[4] = '\0'; 264 | // And write it out 265 | iClient->print((char*)output); 266 | // FIXME We might want to fill output with '=' characters if b64_encode doesn't 267 | // FIXME do it for us when we're encoding the final chunk 268 | inputOffset = 0; 269 | } 270 | } 271 | // And end the header we've sent 272 | iClient->println(); 273 | } 274 | 275 | void HttpClient::finishHeaders() 276 | { 277 | iClient->println(); 278 | iState = eRequestSent; 279 | } 280 | 281 | void HttpClient::endRequest() 282 | { 283 | if (iState < eRequestSent) 284 | { 285 | // We still need to finish off the headers 286 | finishHeaders(); 287 | } 288 | // else the end of headers has already been sent, so nothing to do here 289 | } 290 | 291 | int HttpClient::responseStatusCode() 292 | { 293 | if (iState < eRequestSent) 294 | { 295 | return HTTP_ERROR_API; 296 | } 297 | // The first line will be of the form Status-Line: 298 | // HTTP-Version SP Status-Code SP Reason-Phrase CRLF 299 | // Where HTTP-Version is of the form: 300 | // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT 301 | 302 | char c = '\0'; 303 | do 304 | { 305 | // Make sure the status code is reset, and likewise the state. This 306 | // lets us easily cope with 1xx informational responses by just 307 | // ignoring them really, and reading the next line for a proper response 308 | iStatusCode = 0; 309 | iState = eRequestSent; 310 | 311 | unsigned long timeoutStart = millis(); 312 | // Psuedo-regexp we're expecting before the status-code 313 | const char* statusPrefix = "HTTP/*.* "; 314 | const char* statusPtr = statusPrefix; 315 | // Whilst we haven't timed out & haven't reached the end of the headers 316 | while ((c != '\n') && 317 | ( (millis() - timeoutStart) < iHttpResponseTimeout )) 318 | { 319 | if (available()) 320 | { 321 | c = read(); 322 | if (c != -1) 323 | { 324 | switch(iState) 325 | { 326 | case eRequestSent: 327 | // We haven't reached the status code yet 328 | if ( (*statusPtr == '*') || (*statusPtr == c) ) 329 | { 330 | // This character matches, just move along 331 | statusPtr++; 332 | if (*statusPtr == '\0') 333 | { 334 | // We've reached the end of the prefix 335 | iState = eReadingStatusCode; 336 | } 337 | } 338 | else 339 | { 340 | return HTTP_ERROR_INVALID_RESPONSE; 341 | } 342 | break; 343 | case eReadingStatusCode: 344 | if (isdigit(c)) 345 | { 346 | // This assumes we won't get more than the 3 digits we 347 | // want 348 | iStatusCode = iStatusCode*10 + (c - '0'); 349 | } 350 | else 351 | { 352 | // We've reached the end of the status code 353 | // We could sanity check it here or double-check for ' ' 354 | // rather than anything else, but let's be lenient 355 | iState = eStatusCodeRead; 356 | } 357 | break; 358 | case eStatusCodeRead: 359 | // We're just waiting for the end of the line now 360 | break; 361 | }; 362 | // We read something, reset the timeout counter 363 | timeoutStart = millis(); 364 | } 365 | } 366 | else 367 | { 368 | // We haven't got any data, so let's pause to allow some to 369 | // arrive 370 | delay(kHttpWaitForDataDelay); 371 | } 372 | } 373 | if ( (c == '\n') && (iStatusCode < 200) ) 374 | { 375 | // We've reached the end of an informational status line 376 | c = '\0'; // Clear c so we'll go back into the data reading loop 377 | } 378 | } 379 | // If we've read a status code successfully but it's informational (1xx) 380 | // loop back to the start 381 | while ( (iState == eStatusCodeRead) && (iStatusCode < 200) ); 382 | 383 | if ( (c == '\n') && (iState == eStatusCodeRead) ) 384 | { 385 | // We've read the status-line successfully 386 | return iStatusCode; 387 | } 388 | else if (c != '\n') 389 | { 390 | // We must've timed out before we reached the end of the line 391 | return HTTP_ERROR_TIMED_OUT; 392 | } 393 | else 394 | { 395 | // This wasn't a properly formed status line, or at least not one we 396 | // could understand 397 | return HTTP_ERROR_INVALID_RESPONSE; 398 | } 399 | } 400 | 401 | int HttpClient::skipResponseHeaders() 402 | { 403 | // Just keep reading until we finish reading the headers or time out 404 | unsigned long timeoutStart = millis(); 405 | // Whilst we haven't timed out & haven't reached the end of the headers 406 | while ((!endOfHeadersReached()) && 407 | ( (millis() - timeoutStart) < iHttpResponseTimeout )) 408 | { 409 | if (available()) 410 | { 411 | (void)readHeader(); 412 | // We read something, reset the timeout counter 413 | timeoutStart = millis(); 414 | } 415 | else 416 | { 417 | // We haven't got any data, so let's pause to allow some to 418 | // arrive 419 | delay(kHttpWaitForDataDelay); 420 | } 421 | } 422 | if (endOfHeadersReached()) 423 | { 424 | // Success 425 | return HTTP_SUCCESS; 426 | } 427 | else 428 | { 429 | // We must've timed out 430 | return HTTP_ERROR_TIMED_OUT; 431 | } 432 | } 433 | 434 | bool HttpClient::endOfBodyReached() 435 | { 436 | if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) 437 | { 438 | // We've got to the body and we know how long it will be 439 | return (iBodyLengthConsumed >= contentLength()); 440 | } 441 | return false; 442 | } 443 | 444 | int HttpClient::read() 445 | { 446 | #if 0 // Fails on WiFi because multi-byte read seems to be broken 447 | uint8_t b[1]; 448 | int ret = read(b, 1); 449 | if (ret == 1) 450 | { 451 | return b[0]; 452 | } 453 | else 454 | { 455 | return -1; 456 | } 457 | #else 458 | int ret = iClient->read(); 459 | if (ret >= 0) 460 | { 461 | if (endOfHeadersReached() && iContentLength > 0) 462 | { 463 | // We're outputting the body now and we've seen a Content-Length header 464 | // So keep track of how many bytes are left 465 | iBodyLengthConsumed++; 466 | } 467 | } 468 | return ret; 469 | #endif 470 | } 471 | 472 | int HttpClient::read(uint8_t *buf, size_t size) 473 | { 474 | int ret =iClient->read(buf, size); 475 | if (endOfHeadersReached() && iContentLength > 0) 476 | { 477 | // We're outputting the body now and we've seen a Content-Length header 478 | // So keep track of how many bytes are left 479 | if (ret >= 0) 480 | { 481 | iBodyLengthConsumed += ret; 482 | } 483 | } 484 | return ret; 485 | } 486 | 487 | int HttpClient::readHeader() 488 | { 489 | char c = read(); 490 | 491 | if (endOfHeadersReached()) 492 | { 493 | // We've passed the headers, but rather than return an error, we'll just 494 | // act as a slightly less efficient version of read() 495 | return c; 496 | } 497 | 498 | // Whilst reading out the headers to whoever wants them, we'll keep an 499 | // eye out for the "Content-Length" header 500 | switch(iState) 501 | { 502 | case eStatusCodeRead: 503 | // We're at the start of a line, or somewhere in the middle of reading 504 | // the Content-Length prefix 505 | if (*iContentLengthPtr == c) 506 | { 507 | // This character matches, just move along 508 | iContentLengthPtr++; 509 | if (*iContentLengthPtr == '\0') 510 | { 511 | // We've reached the end of the prefix 512 | iState = eReadingContentLength; 513 | // Just in case we get multiple Content-Length headers, this 514 | // will ensure we just get the value of the last one 515 | iContentLength = 0; 516 | } 517 | } 518 | else if ((iContentLengthPtr == kContentLengthPrefix) && (c == '\r')) 519 | { 520 | // We've found a '\r' at the start of a line, so this is probably 521 | // the end of the headers 522 | iState = eLineStartingCRFound; 523 | } 524 | else 525 | { 526 | // This isn't the Content-Length header, skip to the end of the line 527 | iState = eSkipToEndOfHeader; 528 | } 529 | break; 530 | case eReadingContentLength: 531 | if (isdigit(c)) 532 | { 533 | iContentLength = iContentLength*10 + (c - '0'); 534 | } 535 | else 536 | { 537 | // We've reached the end of the content length 538 | // We could sanity check it here or double-check for "\r\n" 539 | // rather than anything else, but let's be lenient 540 | iState = eSkipToEndOfHeader; 541 | } 542 | break; 543 | case eLineStartingCRFound: 544 | if (c == '\n') 545 | { 546 | iState = eReadingBody; 547 | } 548 | break; 549 | default: 550 | // We're just waiting for the end of the line now 551 | break; 552 | }; 553 | 554 | if ( (c == '\n') && !endOfHeadersReached() ) 555 | { 556 | // We've got to the end of this line, start processing again 557 | iState = eStatusCodeRead; 558 | iContentLengthPtr = kContentLengthPrefix; 559 | } 560 | // And return the character read to whoever wants it 561 | return c; 562 | } 563 | 564 | 565 | 566 | -------------------------------------------------------------------------------- /HttpClient.h: -------------------------------------------------------------------------------- 1 | // Class to simplify HTTP fetching on Arduino 2 | // (c) Copyright MCQN Ltd. 2010-2012 3 | // Released under Apache License, version 2.0 4 | 5 | #ifndef HttpClient_h 6 | #define HttpClient_h 7 | 8 | #include 9 | #include 10 | #include "Client.h" 11 | 12 | static const int HTTP_SUCCESS =0; 13 | // The end of the headers has been reached. This consumes the '\n' 14 | // Could not connect to the server 15 | static const int HTTP_ERROR_CONNECTION_FAILED =-1; 16 | // This call was made when the HttpClient class wasn't expecting it 17 | // to be called. Usually indicates your code is using the class 18 | // incorrectly 19 | static const int HTTP_ERROR_API =-2; 20 | // Spent too long waiting for a reply 21 | static const int HTTP_ERROR_TIMED_OUT =-3; 22 | // The response from the server is invalid, is it definitely an HTTP 23 | // server? 24 | static const int HTTP_ERROR_INVALID_RESPONSE =-4; 25 | 26 | // Define some of the common methods and headers here 27 | // That lets other code reuse them without having to declare another copy 28 | // of them, so saves code space and RAM 29 | #define HTTP_METHOD_GET "GET" 30 | #define HTTP_METHOD_POST "POST" 31 | #define HTTP_METHOD_PUT "PUT" 32 | #define HTTP_METHOD_DELETE "DELETE" 33 | #define HTTP_HEADER_CONTENT_LENGTH "Content-Length" 34 | #define HTTP_HEADER_CONNECTION "Connection" 35 | #define HTTP_HEADER_USER_AGENT "User-Agent" 36 | 37 | class HttpClient : public Client 38 | { 39 | public: 40 | static const int kNoContentLengthHeader =-1; 41 | static const int kHttpPort =80; 42 | static const char* kUserAgent; 43 | 44 | // FIXME Write longer API request, using port and user-agent, example 45 | // FIXME Update tempToPachube example to calculate Content-Length correctly 46 | 47 | #ifdef PROXY_ENABLED // currently disabled as introduces dependency on Dns.h in Ethernet 48 | HttpClient(Client& aClient, const char* aProxy =NULL, uint16_t aProxyPort =0); 49 | #else 50 | HttpClient(Client& aClient); 51 | #endif 52 | 53 | /** Start a more complex request. 54 | Use this when you need to send additional headers in the request, 55 | but you will also need to call endRequest() when you are finished. 56 | */ 57 | void beginRequest(); 58 | 59 | /** End a more complex request. 60 | Use this when you need to have sent additional headers in the request, 61 | but you will also need to call beginRequest() at the start. 62 | */ 63 | void endRequest(); 64 | 65 | /** Connect to the server and start to send a GET request. 66 | @param aServerName Name of the server being connected to. If NULL, the 67 | "Host" header line won't be sent 68 | @param aServerPort Port to connect to on the server 69 | @param aURLPath Url to request 70 | @param aUserAgent User-Agent string to send. If NULL the default 71 | user-agent kUserAgent will be sent 72 | @return 0 if successful, else error 73 | */ 74 | int get(const char* aServerName, uint16_t aServerPort, const char* aURLPath, 75 | const char* aUserAgent =NULL) 76 | { return startRequest(aServerName, aServerPort, aURLPath, HTTP_METHOD_GET, aUserAgent); } 77 | 78 | /** Connect to the server and start to send a GET request. 79 | @param aServerName Name of the server being connected to. If NULL, the 80 | "Host" header line won't be sent 81 | @param aURLPath Url to request 82 | @param aUserAgent User-Agent string to send. If NULL the default 83 | user-agent kUserAgent will be sent 84 | @return 0 if successful, else error 85 | */ 86 | int get(const char* aServerName, const char* aURLPath, const char* aUserAgent =NULL) 87 | { return startRequest(aServerName, kHttpPort, aURLPath, HTTP_METHOD_GET, aUserAgent); } 88 | 89 | /** Connect to the server and start to send a GET request. This version connects 90 | doesn't perform a DNS lookup and just connects to the given IP address. 91 | @param aServerAddress IP address of the server to connect to 92 | @param aServerName Name of the server being connected to. If NULL, the 93 | "Host" header line won't be sent 94 | @param aServerPort Port to connect to on the server 95 | @param aURLPath Url to request 96 | @param aUserAgent User-Agent string to send. If NULL the default 97 | user-agent kUserAgent will be sent 98 | @return 0 if successful, else error 99 | */ 100 | int get(const IPAddress& aServerAddress, 101 | const char* aServerName, 102 | uint16_t aServerPort, 103 | const char* aURLPath, 104 | const char* aUserAgent =NULL) 105 | { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, HTTP_METHOD_GET, aUserAgent); } 106 | 107 | /** Connect to the server and start to send a GET request. This version connects 108 | doesn't perform a DNS lookup and just connects to the given IP address. 109 | @param aServerAddress IP address of the server to connect to 110 | @param aServerName Name of the server being connected to. If NULL, the 111 | "Host" header line won't be sent 112 | @param aURLPath Url to request 113 | @param aUserAgent User-Agent string to send. If NULL the default 114 | user-agent kUserAgent will be sent 115 | @return 0 if successful, else error 116 | */ 117 | int get(const IPAddress& aServerAddress, 118 | const char* aServerName, 119 | const char* aURLPath, 120 | const char* aUserAgent =NULL) 121 | { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, HTTP_METHOD_GET, aUserAgent); } 122 | 123 | /** Connect to the server and start to send a POST request. 124 | @param aServerName Name of the server being connected to. If NULL, the 125 | "Host" header line won't be sent 126 | @param aServerPort Port to connect to on the server 127 | @param aURLPath Url to request 128 | @param aUserAgent User-Agent string to send. If NULL the default 129 | user-agent kUserAgent will be sent 130 | @return 0 if successful, else error 131 | */ 132 | int post(const char* aServerName, 133 | uint16_t aServerPort, 134 | const char* aURLPath, 135 | const char* aUserAgent =NULL) 136 | { return startRequest(aServerName, aServerPort, aURLPath, HTTP_METHOD_POST, aUserAgent); } 137 | 138 | /** Connect to the server and start to send a POST request. 139 | @param aServerName Name of the server being connected to. If NULL, the 140 | "Host" header line won't be sent 141 | @param aURLPath Url to request 142 | @param aUserAgent User-Agent string to send. If NULL the default 143 | user-agent kUserAgent will be sent 144 | @return 0 if successful, else error 145 | */ 146 | int post(const char* aServerName, 147 | const char* aURLPath, 148 | const char* aUserAgent =NULL) 149 | { return startRequest(aServerName, kHttpPort, aURLPath, HTTP_METHOD_POST, aUserAgent); } 150 | 151 | /** Connect to the server and start to send a POST request. This version connects 152 | doesn't perform a DNS lookup and just connects to the given IP address. 153 | @param aServerAddress IP address of the server to connect to 154 | @param aServerName Name of the server being connected to. If NULL, the 155 | "Host" header line won't be sent 156 | @param aServerPort Port to connect to on the server 157 | @param aURLPath Url to request 158 | @param aUserAgent User-Agent string to send. If NULL the default 159 | user-agent kUserAgent will be sent 160 | @return 0 if successful, else error 161 | */ 162 | int post(const IPAddress& aServerAddress, 163 | const char* aServerName, 164 | uint16_t aServerPort, 165 | const char* aURLPath, 166 | const char* aUserAgent =NULL) 167 | { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, HTTP_METHOD_POST, aUserAgent); } 168 | 169 | /** Connect to the server and start to send a POST request. This version connects 170 | doesn't perform a DNS lookup and just connects to the given IP address. 171 | @param aServerAddress IP address of the server to connect to 172 | @param aServerName Name of the server being connected to. If NULL, the 173 | "Host" header line won't be sent 174 | @param aURLPath Url to request 175 | @param aUserAgent User-Agent string to send. If NULL the default 176 | user-agent kUserAgent will be sent 177 | @return 0 if successful, else error 178 | */ 179 | int post(const IPAddress& aServerAddress, 180 | const char* aServerName, 181 | const char* aURLPath, 182 | const char* aUserAgent =NULL) 183 | { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, HTTP_METHOD_POST, aUserAgent); } 184 | 185 | /** Connect to the server and start to send a PUT request. 186 | @param aServerName Name of the server being connected to. If NULL, the 187 | "Host" header line won't be sent 188 | @param aServerPort Port to connect to on the server 189 | @param aURLPath Url to request 190 | @param aUserAgent User-Agent string to send. If NULL the default 191 | user-agent kUserAgent will be sent 192 | @return 0 if successful, else error 193 | */ 194 | int put(const char* aServerName, 195 | uint16_t aServerPort, 196 | const char* aURLPath, 197 | const char* aUserAgent =NULL) 198 | { return startRequest(aServerName, aServerPort, aURLPath, HTTP_METHOD_PUT, aUserAgent); } 199 | 200 | /** Connect to the server and start to send a PUT request. 201 | @param aServerName Name of the server being connected to. If NULL, the 202 | "Host" header line won't be sent 203 | @param aURLPath Url to request 204 | @param aUserAgent User-Agent string to send. If NULL the default 205 | user-agent kUserAgent will be sent 206 | @return 0 if successful, else error 207 | */ 208 | int put(const char* aServerName, 209 | const char* aURLPath, 210 | const char* aUserAgent =NULL) 211 | { return startRequest(aServerName, kHttpPort, aURLPath, HTTP_METHOD_PUT, aUserAgent); } 212 | 213 | /** Connect to the server and start to send a PUT request. This version connects 214 | doesn't perform a DNS lookup and just connects to the given IP address. 215 | @param aServerAddress IP address of the server to connect to 216 | @param aServerName Name of the server being connected to. If NULL, the 217 | "Host" header line won't be sent 218 | @param aServerPort Port to connect to on the server 219 | @param aURLPath Url to request 220 | @param aUserAgent User-Agent string to send. If NULL the default 221 | user-agent kUserAgent will be sent 222 | @return 0 if successful, else error 223 | */ 224 | int put(const IPAddress& aServerAddress, 225 | const char* aServerName, 226 | uint16_t aServerPort, 227 | const char* aURLPath, 228 | const char* aUserAgent =NULL) 229 | { return startRequest(aServerAddress, aServerName, aServerPort, aURLPath, HTTP_METHOD_PUT, aUserAgent); } 230 | 231 | /** Connect to the server and start to send a PUT request. This version connects 232 | doesn't perform a DNS lookup and just connects to the given IP address. 233 | @param aServerAddress IP address of the server to connect to 234 | @param aServerName Name of the server being connected to. If NULL, the 235 | "Host" header line won't be sent 236 | @param aURLPath Url to request 237 | @param aUserAgent User-Agent string to send. If NULL the default 238 | user-agent kUserAgent will be sent 239 | @return 0 if successful, else error 240 | */ 241 | int put(const IPAddress& aServerAddress, 242 | const char* aServerName, 243 | const char* aURLPath, 244 | const char* aUserAgent =NULL) 245 | { return startRequest(aServerAddress, aServerName, kHttpPort, aURLPath, HTTP_METHOD_PUT, aUserAgent); } 246 | 247 | /** Connect to the server and start to send the request. 248 | @param aServerName Name of the server being connected to. 249 | @param aServerPort Port to connect to on the server 250 | @param aURLPath Url to request 251 | @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. 252 | @param aUserAgent User-Agent string to send. If NULL the default 253 | user-agent kUserAgent will be sent 254 | @return 0 if successful, else error 255 | */ 256 | int startRequest(const char* aServerName, 257 | uint16_t aServerPort, 258 | const char* aURLPath, 259 | const char* aHttpMethod, 260 | const char* aUserAgent); 261 | 262 | /** Connect to the server and start to send the request. 263 | @param aServerAddress IP address of the server to connect to. 264 | @param aServerName Name of the server being connected to. If NULL, the 265 | "Host" header line won't be sent 266 | @param aServerPort Port to connect to on the server 267 | @param aURLPath Url to request 268 | @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. 269 | @param aUserAgent User-Agent string to send. If NULL the default 270 | user-agent kUserAgent will be sent 271 | @return 0 if successful, else error 272 | */ 273 | int startRequest(const IPAddress& aServerAddress, 274 | const char* aServerName, 275 | uint16_t aServerPort, 276 | const char* aURLPath, 277 | const char* aHttpMethod, 278 | const char* aUserAgent); 279 | 280 | /** Send an additional header line. This can only be called in between the 281 | calls to startRequest and finishRequest. 282 | @param aHeader Header line to send, in its entirety (but without the 283 | trailing CRLF. E.g. "Authorization: Basic YQDDCAIGES" 284 | */ 285 | void sendHeader(const char* aHeader); 286 | 287 | /** Send an additional header line. This is an alternate form of 288 | sendHeader() which takes the header name and content as separate strings. 289 | The call will add the ": " to separate the header, so for example, to 290 | send a XXXXXX header call sendHeader("XXXXX", "Something") 291 | @param aHeaderName Type of header being sent 292 | @param aHeaderValue Value for that header 293 | */ 294 | void sendHeader(const char* aHeaderName, const char* aHeaderValue); 295 | 296 | /** Send an additional header line. This is an alternate form of 297 | sendHeader() which takes the header name and content separately but where 298 | the value is provided as an integer. 299 | The call will add the ": " to separate the header, so for example, to 300 | send a XXXXXX header call sendHeader("XXXXX", 123) 301 | @param aHeaderName Type of header being sent 302 | @param aHeaderValue Value for that header 303 | */ 304 | void sendHeader(const char* aHeaderName, const int aHeaderValue); 305 | 306 | /** Send a basic authentication header. This will encode the given username 307 | and password, and send them in suitable header line for doing Basic 308 | Authentication. 309 | @param aUser Username for the authorization 310 | @param aPassword Password for the user aUser 311 | */ 312 | void sendBasicAuth(const char* aUser, const char* aPassword); 313 | 314 | /** Finish sending the HTTP request. This basically just sends the blank 315 | line to signify the end of the request 316 | */ 317 | void finishRequest(); 318 | 319 | /** Get the HTTP status code contained in the response. 320 | For example, 200 for successful request, 404 for file not found, etc. 321 | */ 322 | int responseStatusCode(); 323 | 324 | /** Read the next character of the response headers. 325 | This functions in the same way as read() but to be used when reading 326 | through the headers. Check whether or not the end of the headers has 327 | been reached by calling endOfHeadersReached(), although after that point 328 | this will still return data as read() would, but slightly less efficiently 329 | @return The next character of the response headers 330 | */ 331 | int readHeader(); 332 | 333 | /** Skip any response headers to get to the body. 334 | Use this if you don't want to do any special processing of the headers 335 | returned in the response. You can also use it after you've found all of 336 | the headers you're interested in, and just want to get on with processing 337 | the body. 338 | @return HTTP_SUCCESS if successful, else an error code 339 | */ 340 | int skipResponseHeaders(); 341 | 342 | /** Test whether all of the response headers have been consumed. 343 | @return true if we are now processing the response body, else false 344 | */ 345 | bool endOfHeadersReached() { return (iState == eReadingBody); }; 346 | 347 | /** Test whether the end of the body has been reached. 348 | Only works if the Content-Length header was returned by the server 349 | @return true if we are now at the end of the body, else false 350 | */ 351 | bool endOfBodyReached(); 352 | virtual bool endOfStream() { return endOfBodyReached(); }; 353 | virtual bool completed() { return endOfBodyReached(); }; 354 | 355 | /** Return the length of the body. 356 | @return Length of the body, in bytes, or kNoContentLengthHeader if no 357 | Content-Length header was returned by the server 358 | */ 359 | int contentLength() { return iContentLength; }; 360 | 361 | // Inherited from Print 362 | // Note: 1st call to these indicates the user is sending the body, so if need 363 | // Note: be we should finish the header first 364 | virtual size_t write(uint8_t aByte) { if (iState < eRequestSent) { finishHeaders(); }; return iClient-> write(aByte); }; 365 | virtual size_t write(const uint8_t *aBuffer, size_t aSize) { if (iState < eRequestSent) { finishHeaders(); }; return iClient->write(aBuffer, aSize); }; 366 | // Inherited from Stream 367 | virtual int available() { return iClient->available(); }; 368 | /** Read the next byte from the server. 369 | @return Byte read or -1 if there are no bytes available. 370 | */ 371 | virtual int read(); 372 | virtual int read(uint8_t *buf, size_t size); 373 | virtual int peek() { return iClient->peek(); }; 374 | virtual void flush() { return iClient->flush(); }; 375 | 376 | // Inherited from Client 377 | virtual int connect(IPAddress ip, uint16_t port) { return iClient->connect(ip, port); }; 378 | virtual int connect(const char *host, uint16_t port) { return iClient->connect(host, port); }; 379 | virtual void stop(); 380 | virtual uint8_t connected() { return iClient->connected(); }; 381 | virtual operator bool() { return bool(iClient); }; 382 | virtual uint32_t httpResponseTimeout() { return iHttpResponseTimeout; }; 383 | virtual void setHttpResponseTimeout(uint32_t timeout) { iHttpResponseTimeout = timeout; }; 384 | protected: 385 | /** Reset internal state data back to the "just initialised" state 386 | */ 387 | void resetState(); 388 | 389 | /** Send the first part of the request and the initial headers. 390 | @param aServerName Name of the server being connected to. If NULL, the 391 | "Host" header line won't be sent 392 | @param aServerIP IP address of the server (only used if we're going through a 393 | proxy and aServerName is NULL 394 | @param aServerPort Port of the server being connected to. 395 | @param aURLPath Url to request 396 | @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. 397 | @param aUserAgent User-Agent string to send. If NULL the default 398 | user-agent kUserAgent will be sent 399 | @return 0 if successful, else error 400 | */ 401 | int sendInitialHeaders(const char* aServerName, 402 | IPAddress aServerIP, 403 | uint16_t aPort, 404 | const char* aURLPath, 405 | const char* aHttpMethod, 406 | const char* aUserAgent); 407 | 408 | /* Let the server know that we've reached the end of the headers 409 | */ 410 | void finishHeaders(); 411 | 412 | // Number of milliseconds that we wait each time there isn't any data 413 | // available to be read (during status code and header processing) 414 | static const int kHttpWaitForDataDelay = 1000; 415 | // Number of milliseconds that we'll wait in total without receiveing any 416 | // data before returning HTTP_ERROR_TIMED_OUT (during status code and header 417 | // processing) 418 | static const int kHttpResponseTimeout = 30*1000; 419 | static const char* kContentLengthPrefix; 420 | typedef enum { 421 | eIdle, 422 | eRequestStarted, 423 | eRequestSent, 424 | eReadingStatusCode, 425 | eStatusCodeRead, 426 | eReadingContentLength, 427 | eSkipToEndOfHeader, 428 | eLineStartingCRFound, 429 | eReadingBody 430 | } tHttpState; 431 | // Ethernet client we're using 432 | Client* iClient; 433 | // Current state of the finite-state-machine 434 | tHttpState iState; 435 | // Stores the status code for the response, once known 436 | int iStatusCode; 437 | // Stores the value of the Content-Length header, if present 438 | int iContentLength; 439 | // How many bytes of the response body have been read by the user 440 | int iBodyLengthConsumed; 441 | // How far through a Content-Length header prefix we are 442 | const char* iContentLengthPtr; 443 | // Address of the proxy to use, if we're using one 444 | IPAddress iProxyAddress; 445 | uint16_t iProxyPort; 446 | uint32_t iHttpResponseTimeout; 447 | }; 448 | 449 | #endif 450 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HttpClient 2 | 3 | HttpClient is a library to make it easier to interact with web servers from Arduino. 4 | 5 | ## Dependencies 6 | 7 | - Requires the new Ethernet library API (with DHCP and DNS) which is in Arduino 1.0 and later 8 | 9 | ## Installation 10 | 11 | 1. Download the latest version of the library from https://github.com/amcewen/HttpClient/releases and save the file somewhere 12 | 1. In the Arduino IDE, go to the Sketch -> Import Library -> Add Library... menu option 13 | 1. Find the zip file that you saved in the first step, and choose that 14 | 1. Check that it has been successfully added by opening the Sketch -> Import Library menu. You should now see HttpClient listed among the available libraries. 15 | 16 | ## Usage 17 | 18 | In normal usage, handles the outgoing request and Host header. The returned status code is parsed for you, as is the Content-Length header (if present). 19 | 20 | Because it expects an object of type Client, you can use it with any of the networking classes that derive from that. Which means it will work with EthernetClient, WiFiClient and GSMClient. 21 | 22 | See the examples for more detail on how the library is used. 23 | 24 | -------------------------------------------------------------------------------- /b64.cpp: -------------------------------------------------------------------------------- 1 | // Simple Base64 code 2 | // (c) Copyright 2010 MCQN Ltd. 3 | // Released under Apache License, version 2.0 4 | 5 | #include "b64.h" 6 | 7 | /* Simple test program 8 | #include 9 | void main() 10 | { 11 | char* in = "amcewen"; 12 | char out[22]; 13 | 14 | b64_encode(in, 15, out, 22); 15 | out[21] = '\0'; 16 | 17 | printf(out); 18 | } 19 | */ 20 | 21 | int b64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen) 22 | { 23 | // Work out if we've got enough space to encode the input 24 | // Every 6 bits of input becomes a byte of output 25 | if (aOutputLen < (aInputLen*8)/6) 26 | { 27 | // FIXME Should we return an error here, or just the length 28 | return (aInputLen*8)/6; 29 | } 30 | 31 | // If we get here we've got enough space to do the encoding 32 | 33 | const char* b64_dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 34 | if (aInputLen == 3) 35 | { 36 | aOutput[0] = b64_dictionary[aInput[0] >> 2]; 37 | aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4|(aInput[1]>>4)]; 38 | aOutput[2] = b64_dictionary[(aInput[1]&0x0F)<<2|(aInput[2]>>6)]; 39 | aOutput[3] = b64_dictionary[aInput[2]&0x3F]; 40 | } 41 | else if (aInputLen == 2) 42 | { 43 | aOutput[0] = b64_dictionary[aInput[0] >> 2]; 44 | aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4|(aInput[1]>>4)]; 45 | aOutput[2] = b64_dictionary[(aInput[1]&0x0F)<<2]; 46 | aOutput[3] = '='; 47 | } 48 | else if (aInputLen == 1) 49 | { 50 | aOutput[0] = b64_dictionary[aInput[0] >> 2]; 51 | aOutput[1] = b64_dictionary[(aInput[0] & 0x3)<<4]; 52 | aOutput[2] = '='; 53 | aOutput[3] = '='; 54 | } 55 | else 56 | { 57 | // Break the input into 3-byte chunks and process each of them 58 | int i; 59 | for (i = 0; i < aInputLen/3; i++) 60 | { 61 | b64_encode(&aInput[i*3], 3, &aOutput[i*4], 4); 62 | } 63 | if (aInputLen % 3 > 0) 64 | { 65 | // It doesn't fit neatly into a 3-byte chunk, so process what's left 66 | b64_encode(&aInput[i*3], aInputLen % 3, &aOutput[i*4], aOutputLen - (i*4)); 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /b64.h: -------------------------------------------------------------------------------- 1 | #ifndef b64_h 2 | #define b64_h 3 | 4 | int b64_encode(const unsigned char* aInput, int aInputLen, unsigned char* aOutput, int aOutputLen); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /examples/SimpleHttpExample/SimpleHttpExample.ino: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2010-2012 MCQN Ltd. 2 | // Released under Apache License, version 2.0 3 | // 4 | // Simple example to show how to use the HttpClient library 5 | // Get's the web page given at http:// and 6 | // outputs the content to the serial port 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // This example downloads the URL "http://arduino.cc/" 14 | 15 | // Name of the server we want to connect to 16 | const char kHostname[] = "arduino.cc"; 17 | // Path to download (this is the bit after the hostname in the URL 18 | // that you want to download 19 | const char kPath[] = "/"; 20 | 21 | byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; 22 | 23 | // Number of milliseconds to wait without receiving any data before we give up 24 | const int kNetworkTimeout = 30*1000; 25 | // Number of milliseconds to wait if no data is available before trying again 26 | const int kNetworkDelay = 1000; 27 | 28 | void setup() 29 | { 30 | // initialize serial communications at 9600 bps: 31 | Serial.begin(9600); 32 | 33 | while (Ethernet.begin(mac) != 1) 34 | { 35 | Serial.println("Error getting IP address via DHCP, trying again..."); 36 | delay(15000); 37 | } 38 | } 39 | 40 | void loop() 41 | { 42 | int err =0; 43 | 44 | EthernetClient c; 45 | HttpClient http(c); 46 | 47 | err = http.get(kHostname, kPath); 48 | if (err == 0) 49 | { 50 | Serial.println("startedRequest ok"); 51 | 52 | err = http.responseStatusCode(); 53 | if (err >= 0) 54 | { 55 | Serial.print("Got status code: "); 56 | Serial.println(err); 57 | 58 | // Usually you'd check that the response code is 200 or a 59 | // similar "success" code (200-299) before carrying on, 60 | // but we'll print out whatever response we get 61 | 62 | err = http.skipResponseHeaders(); 63 | if (err >= 0) 64 | { 65 | int bodyLen = http.contentLength(); 66 | Serial.print("Content length is: "); 67 | Serial.println(bodyLen); 68 | Serial.println(); 69 | Serial.println("Body returned follows:"); 70 | 71 | // Now we've got to the body, so we can print it out 72 | unsigned long timeoutStart = millis(); 73 | char c; 74 | // Whilst we haven't timed out & haven't reached the end of the body 75 | while ( (http.connected() || http.available()) && 76 | ((millis() - timeoutStart) < kNetworkTimeout) ) 77 | { 78 | if (http.available()) 79 | { 80 | c = http.read(); 81 | // Print out this character 82 | Serial.print(c); 83 | 84 | bodyLen--; 85 | // We read something, reset the timeout counter 86 | timeoutStart = millis(); 87 | } 88 | else 89 | { 90 | // We haven't got any data, so let's pause to allow some to 91 | // arrive 92 | delay(kNetworkDelay); 93 | } 94 | } 95 | } 96 | else 97 | { 98 | Serial.print("Failed to skip response headers: "); 99 | Serial.println(err); 100 | } 101 | } 102 | else 103 | { 104 | Serial.print("Getting response failed: "); 105 | Serial.println(err); 106 | } 107 | } 108 | else 109 | { 110 | Serial.print("Connect failed: "); 111 | Serial.println(err); 112 | } 113 | http.stop(); 114 | 115 | // And just stop, now that we've tried a download 116 | while(1); 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map For HttpClient 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | 9 | HttpClient KEYWORD1 10 | 11 | ####################################### 12 | # Methods and Functions (KEYWORD2) 13 | ####################################### 14 | 15 | get KEYWORD2 16 | post KEYWORD2 17 | put KEYWORD2 18 | startRequest KEYWORD2 19 | beginRequest KEYWORD2 20 | sendHeader KEYWORD2 21 | sendBasicAuth KEYWORD2 22 | endRequest KEYWORD2 23 | responseStatusCode KEYWORD2 24 | readHeader KEYWORD2 25 | skipResponseHeaders KEYWORD2 26 | endOfHeadersReached KEYWORD2 27 | endOfBodyReached KEYWORD2 28 | completed KEYWORD2 29 | contentLength KEYWORD2 30 | 31 | ####################################### 32 | # Constants (LITERAL1) 33 | ####################################### 34 | HTTP_SUCCESS LITERAL1 35 | HTTP_ERROR_CONNECTION_FAILED LITERAL1 36 | HTTP_ERROR_API LITERAL1 37 | HTTP_ERROR_TIMED_OUT LITERAL1 38 | HTTP_ERROR_INVALID_RESPONSE LITERAL1 39 | 40 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=HttpClient 2 | version=2.2.0 3 | author=Adrian McEwen 4 | maintainer=Adrian McEwen 5 | sentence=Library to easily make HTTP GET, POST and PUT requests to a web server. 6 | paragraph=Works with any class derived from Client - so switching between Ethernet, WiFi and GSMClient requires minimal code changes. 7 | category=Communication 8 | url=http://github.com/amcewen/HttpClient 9 | architectures=* 10 | --------------------------------------------------------------------------------