Skip to content

Add Cookies to HTTPClient #2186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};