Skip to content

Commit

Permalink
Add Cookies to HTTPClient (#2186)
Browse files Browse the repository at this point in the history
  • Loading branch information
earlephilhower authored May 30, 2024
1 parent 56bd539 commit 0a2b616
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 10 deletions.
215 changes: 207 additions & 8 deletions libraries/HTTPClient/src/HTTPClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
*/
#include <Arduino.h>

#include "HTTPClient.h"
#include <WiFi.h>
#include <time.h>
#include "base64.h"
extern "C" char *strptime(const char *__restrict, const char *__restrict, struct tm *__restrict); // Not exposed by headers?


// per https://github.com/esp8266/Arduino/issues/8231
// make sure HTTPClient can be utilized as a movable class member
Expand Down Expand Up @@ -205,6 +207,7 @@ bool HTTPClient::begin(WiFiClient &client, const String& url) {
}

_port = (protocol == "https" ? 443 : 80);
_secure = (protocol == "https");
_clientIn = client.clone();
_clientGiven = true;
if (_clientMade) {
Expand Down Expand Up @@ -246,6 +249,7 @@ bool HTTPClient::begin(WiFiClient &client, const String& host, uint16_t port, co
_port = port;
_uri = uri;
_protocol = (https ? "https" : "http");
_secure = https;
return true;
}

Expand Down Expand Up @@ -590,6 +594,12 @@ int HTTPClient::sendRequest(const char * type, const uint8_t * payload, size_t s

addHeader(F("Content-Length"), String(payload && size > 0 ? size : 0));

// add cookies to header, if present
String cookie_string;
if (generateCookieString(&cookie_string)) {
addHeader("Cookie", cookie_string);
}

// send Header
if (!sendHeader(type)) {
return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
Expand Down Expand Up @@ -691,6 +701,12 @@ int HTTPClient::sendRequest(const char * type, Stream * stream, size_t size) {
addHeader(F("Content-Length"), String(size));
}

// add cookies to header, if present
String cookie_string;
if (generateCookieString(&cookie_string)) {
addHeader("Cookie", cookie_string);
}

// send Header
if (!sendHeader(type)) {
return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
Expand Down Expand Up @@ -1100,6 +1116,7 @@ int HTTPClient::handleHeaderResponse() {

_transferEncoding = HTTPC_TE_IDENTITY;
unsigned long lastDataTime = millis();
String date;

while (connected()) {
size_t len = _client()->available();
Expand Down Expand Up @@ -1127,6 +1144,10 @@ int HTTPClient::handleHeaderResponse() {
_size = headerValue.toInt();
}

if (headerName.equalsIgnoreCase("Date")) {
date = headerValue;
}

if (_canReuse && headerName.equalsIgnoreCase(F("Connection"))) {
if (headerValue.indexOf(F("close")) >= 0 &&
headerValue.indexOf(F("keep-alive")) < 0) {
Expand All @@ -1142,15 +1163,20 @@ int HTTPClient::handleHeaderResponse() {
_location = headerValue;
}

if (headerName.equalsIgnoreCase("Set-Cookie")) {
setCookie(date, headerValue);
}

for (size_t i = 0; i < _headerKeysCount; i++) {
if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
if (_currentHeaders[i].value != "") {
// Existing value, append this one with a comma
_currentHeaders[i].value += ',';
_currentHeaders[i].value += headerValue;
} else {
_currentHeaders[i].value = headerValue;
}
// Uncomment the following lines if you need to add support for multiple headers with the same key:
// if (!_currentHeaders[i].value.isEmpty()) {
// // Existing value, append this one with a comma
// _currentHeaders[i].value += ',';
// _currentHeaders[i].value += headerValue;
// } else {
_currentHeaders[i].value = headerValue;
// }
break; // We found a match, stop looking
}
}
Expand Down Expand Up @@ -1210,3 +1236,176 @@ int HTTPClient::returnError(int error) {
}
return error;
}

void HTTPClient::setCookieJar(CookieJar* cookieJar) {
_cookieJar = cookieJar;
}

void HTTPClient::resetCookieJar() {
_cookieJar = nullptr;
}

void HTTPClient::clearAllCookies() {
if (_cookieJar) {
_cookieJar->clear();
}
}

void HTTPClient::setCookie(String date, String headerValue) {
if (!_cookieJar) {
return;
}

#define HTTP_TIME_PATTERN "%a, %d %b %Y %H:%M:%S"

Cookie cookie;
String value;
int pos1, pos2;

struct tm tm;
strptime(date.c_str(), HTTP_TIME_PATTERN, &tm);
cookie.date = mktime(&tm);

pos1 = headerValue.indexOf('=');
pos2 = headerValue.indexOf(';');

if (pos1 >= 0 && pos2 > pos1) {
cookie.name = headerValue.substring(0, pos1);
cookie.value = headerValue.substring(pos1 + 1, pos2);
} else {
return; // invalid cookie header
}

// only Cookie Attributes are case insensitive from this point on
headerValue.toLowerCase();

// expires
if (headerValue.indexOf("expires=") >= 0) {
pos1 = headerValue.indexOf("expires=") + strlen("expires=");
pos2 = headerValue.indexOf(';', pos1);

if (pos2 > pos1) {
value = headerValue.substring(pos1, pos2);
} else {
value = headerValue.substring(pos1);
}

strptime(value.c_str(), HTTP_TIME_PATTERN, &tm);
cookie.expires.date = mktime(&tm);
cookie.expires.valid = true;
}

// max-age
if (headerValue.indexOf("max-age=") >= 0) {
pos1 = headerValue.indexOf("max-age=") + strlen("max-age=");
pos2 = headerValue.indexOf(';', pos1);

if (pos2 > pos1) {
value = headerValue.substring(pos1, pos2);
} else {
value = headerValue.substring(pos1);
}

cookie.max_age.duration = value.toInt();
cookie.max_age.valid = true;
}

// domain
if (headerValue.indexOf("domain=") >= 0) {
pos1 = headerValue.indexOf("domain=") + strlen("domain=");
pos2 = headerValue.indexOf(';', pos1);

if (pos2 > pos1) {
value = headerValue.substring(pos1, pos2);
} else {
value = headerValue.substring(pos1);
}

if (value.startsWith(".")) {
value.remove(0, 1);
}

if (_host.indexOf(value) >= 0) {
cookie.domain = value;
} else {
return; // server tries to set a cookie on a different domain; ignore it
}
} else {
pos1 = _host.lastIndexOf('.', _host.lastIndexOf('.') - 1);
if (pos1 >= 0) {
cookie.domain = _host.substring(pos1 + 1);
} else {
cookie.domain = _host;
}
}

// path
if (headerValue.indexOf("path=") >= 0) {
pos1 = headerValue.indexOf("path=") + strlen("path=");
pos2 = headerValue.indexOf(';', pos1);

if (pos2 > pos1) {
cookie.path = headerValue.substring(pos1, pos2);
} else {
cookie.path = headerValue.substring(pos1);
}
}

// HttpOnly
cookie.http_only = (headerValue.indexOf("httponly") >= 0);

// secure
cookie.secure = (headerValue.indexOf("secure") >= 0);

// overwrite or delete cookie in/from cookie jar
time_t now_local = time(NULL);
time_t now_gmt = mktime(gmtime(&now_local));

bool found = false;

for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
if (c->domain == cookie.domain && c->name == cookie.name) {
// when evaluating, max-age takes precedence over expires if both are defined
if ((cookie.max_age.valid && ((cookie.date + cookie.max_age.duration) < now_gmt || (cookie.max_age.duration <= 0)))
|| (!cookie.max_age.valid && cookie.expires.valid && (cookie.expires.date < now_gmt))) {
_cookieJar->erase(c);
c--;
} else {
*c = cookie;
}
found = true;
}
}

// add cookie to jar
if (!found && !(cookie.max_age.valid && cookie.max_age.duration <= 0)) {
_cookieJar->push_back(cookie);
}

}

bool HTTPClient::generateCookieString(String *cookieString) {
if (!_cookieJar) {
return false;
}
time_t now_local = time(NULL);
time_t now_gmt = mktime(gmtime(&now_local));

*cookieString = "";
bool found = false;

for (auto c = _cookieJar->begin(); c != _cookieJar->end(); ++c) {
if ((c->max_age.valid && ((c->date + c->max_age.duration) < now_gmt)) || (!c->max_age.valid && c->expires.valid && (c->expires.date < now_gmt))) {
_cookieJar->erase(c);
c--;
} else if (_host.indexOf(c->domain) >= 0 && (!c->secure || _secure)) {
if (*cookieString == "") {
*cookieString = c->name + "=" + c->value;
} else {
*cookieString += " ;" + c->name + "=" + c->value;
}
found = true;
}
}
return found;
}
37 changes: 35 additions & 2 deletions libraries/HTTPClient/src/HTTPClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <WiFiClientSecure.h>

#include <memory>
#include <vector>

#ifdef DEBUG_ESP_HTTP_CLIENT
#ifdef DEBUG_ESP_PORT
Expand Down Expand Up @@ -153,6 +154,28 @@ typedef enum {
class TransportTraits;
typedef std::unique_ptr<TransportTraits> TransportTraitsPtr;


// cookie jar support
typedef struct {
String host; // host which tries to set the cookie
time_t date; // timestamp of the response that set the cookie
String name;
String value;
String domain;
String path = "";
struct {
time_t date = 0;
bool valid = false;
} expires;
struct {
time_t duration = 0;
bool valid = false;
} max_age;
bool http_only = false;
bool secure = false;
} Cookie;
typedef std::vector<Cookie> CookieJar;

class HTTPClient {
public:
HTTPClient() = default;
Expand Down Expand Up @@ -229,6 +252,11 @@ class HTTPClient {
const String& getString(void);
static String errorToString(int error);

// Cookie jar support
void setCookieJar(CookieJar* cookieJar);
void resetCookieJar();
void clearAllCookies();

// ----------------------------------------------------------------------------------------------
// HTTPS support, mirrors the WiFiClientSecure interface
// Could possibly use a virtual interface class between the two, but for now it is more
Expand Down Expand Up @@ -327,6 +355,10 @@ class HTTPClient {
int handleHeaderResponse();
int writeToStreamDataBlock(Stream * stream, int len);

// Cookie jar support
void setCookie(String date, String headerValue);
bool generateCookieString(String *cookieString);

WiFiClient *_clientMade = nullptr;
bool _clientTLS = false;

Expand All @@ -350,6 +382,7 @@ class HTTPClient {

String _uri;
String _protocol;
bool _secure = false;
String _headers;
String _base64Authorization;

Expand All @@ -368,6 +401,6 @@ class HTTPClient {
String _location;
transferEncoding_t _transferEncoding = HTTPC_TE_IDENTITY;
std::unique_ptr<StreamString> _payload;


// Cookie jar support
CookieJar *_cookieJar = nullptr;
};

0 comments on commit 0a2b616

Please sign in to comment.