public-Homekit_Split/001-homekit_hub/lib/HttpClient/HttpClient.cpp
Zygfryd Homonto 5f1b6cd8f0 public
2024-07-20 21:58:04 +01:00

566 lines
16 KiB
C++

// 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"
#ifdef PROXY_ENABLED // currently disabled as introduces dependency on Dns.h in Ethernet
#include <Dns.h>
#endif
// Initialize constants
const char* HttpClient::kUserAgent = "Arduino/2.2.0";
const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": ";
#ifdef PROXY_ENABLED // currently disabled as introduces dependency on Dns.h in Ethernet
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);
}
}
#else
HttpClient::HttpClient(Client& aClient)
: iClient(&aClient), iProxyPort(0)
{
resetState();
}
#endif
void HttpClient::resetState()
{
iState = eIdle;
iStatusCode = 0;
iContentLength = 0;
iBodyLengthConsumed = 0;
iContentLengthPtr = kContentLengthPrefix;
iHttpResponseTimeout = kHttpResponseTimeout;
}
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;
}
#ifdef PROXY_ENABLED
if (iProxyPort)
{
if (!iClient->connect(iProxyAddress, iProxyPort) > 0)
{
#ifdef LOGGING
Serial.println("Proxy connection failed");
#endif
return HTTP_ERROR_CONNECTION_FAILED;
}
}
else
#endif
{
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;
}
#ifdef PROXY_ENABLED
if (iProxyPort)
{
if (!iClient->connect(iProxyAddress, iProxyPort) > 0)
{
#ifdef LOGGING
Serial.println("Proxy connection failed");
#endif
return HTTP_ERROR_CONNECTION_FAILED;
}
}
else
#endif
{
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(" ");
#ifdef PROXY_ENABLED
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);
}
}
#endif
iClient->print(aURLPath);
iClient->println(" HTTP/1.1");
// The host header, if required
if (aServerName)
{
iClient->print("Host: ");
iClient->print(aServerName);
if (aPort != kHttpPort)
{
iClient->print(":");
iClient->print(aPort);
}
iClient->println();
}
// And user-agent string
if (aUserAgent)
{
sendHeader(HTTP_HEADER_USER_AGENT, aUserAgent);
}
else
{
sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent);
}
// We don't support persistent connections, so tell the server to
// close this connection after we're done
sendHeader(HTTP_HEADER_CONNECTION, "close");
// 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) < iHttpResponseTimeout ))
{
if (available())
{
c = read();
if (c != -1)
{
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) < iHttpResponseTimeout ))
{
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()
{
#if 0 // Fails on WiFi because multi-byte read seems to be broken
uint8_t b[1];
int ret = read(b, 1);
if (ret == 1)
{
return b[0];
}
else
{
return -1;
}
#else
int ret = iClient->read();
if (ret >= 0)
{
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
iBodyLengthConsumed++;
}
}
return ret;
#endif
}
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;
}