Skip to content

Add support for following redirects in HTTPClient #4240

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 1 commit into from
Oct 1, 2020
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
162 changes: 145 additions & 17 deletions libraries/HTTPClient/src/HTTPClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -548,29 +548,106 @@ int HTTPClient::sendRequest(const char * type, String payload)
*/
int HTTPClient::sendRequest(const char * type, uint8_t * payload, size_t size)
{
// connect to server
if(!connect()) {
return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
}
int code;
bool redirect = false;
uint16_t redirectCount = 0;
do {
// wipe out any existing headers from previous request
for(size_t i = 0; i < _headerKeysCount; i++) {
if (_currentHeaders[i].value.length() > 0) {
_currentHeaders[i].value.clear();
}
}

if(payload && size > 0) {
addHeader(F("Content-Length"), String(size));
}
log_d("request type: '%s' redirCount: %d\n", type, redirectCount);

// connect to server
if(!connect()) {
return returnError(HTTPC_ERROR_CONNECTION_REFUSED);
}

// send Header
if(!sendHeader(type)) {
return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
}
if(payload && size > 0) {
addHeader(F("Content-Length"), String(size));
}

// send Payload if needed
if(payload && size > 0) {
if(_client->write(&payload[0], size) != size) {
return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
// send Header
if(!sendHeader(type)) {
return returnError(HTTPC_ERROR_SEND_HEADER_FAILED);
}

// send Payload if needed
if(payload && size > 0) {
if(_client->write(&payload[0], size) != size) {
return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED);
}
}

code = handleHeaderResponse();
Serial.printf("sendRequest code=%d\n", code);

// Handle redirections as stated in RFC document:
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
//
// Implementing HTTP_CODE_FOUND as redirection with GET method,
// to follow most of existing user agent implementations.
//
redirect = false;
if (
_followRedirects != HTTPC_DISABLE_FOLLOW_REDIRECTS &&
redirectCount < _redirectLimit &&
_location.length() > 0
) {
switch (code) {
// redirecting using the same method
case HTTP_CODE_MOVED_PERMANENTLY:
case HTTP_CODE_TEMPORARY_REDIRECT: {
if (
// allow to force redirections on other methods
// (the RFC require user to accept the redirection)
_followRedirects == HTTPC_FORCE_FOLLOW_REDIRECTS ||
// allow GET and HEAD methods without force
!strcmp(type, "GET") ||
!strcmp(type, "HEAD")
) {
redirectCount += 1;
log_d("following redirect (the same method): '%s' redirCount: %d\n", _location.c_str(), redirectCount);
if (!setURL(_location)) {
log_d("failed setting URL for redirection\n");
// no redirection
break;
}
// redirect using the same request method and payload, diffrent URL
redirect = true;
}
break;
}
// redirecting with method dropped to GET or HEAD
// note: it does not need `HTTPC_FORCE_FOLLOW_REDIRECTS` for any method
case HTTP_CODE_FOUND:
case HTTP_CODE_SEE_OTHER: {
redirectCount += 1;
log_d("following redirect (dropped to GET/HEAD): '%s' redirCount: %d\n", _location.c_str(), redirectCount);
if (!setURL(_location)) {
log_d("failed setting URL for redirection\n");
// no redirection
break;
}
// redirect after changing method to GET/HEAD and dropping payload
type = "GET";
payload = nullptr;
size = 0;
redirect = true;
break;
}

default:
break;
}
}
}

} while (redirect);
// handle Server Response (Header)
return returnError(handleHeaderResponse());
return returnError(code);
}

/**
Expand Down Expand Up @@ -1145,6 +1222,10 @@ int HTTPClient::handleHeaderResponse()
transferEncoding = headerValue;
}

if (headerName.equalsIgnoreCase("Location")) {
_location = headerValue;
}

for(size_t i = 0; i < _headerKeysCount; i++) {
if(_currentHeaders[i].key.equalsIgnoreCase(headerName)) {
_currentHeaders[i].value = headerValue;
Expand Down Expand Up @@ -1322,3 +1403,50 @@ int HTTPClient::returnError(int error)
}
return error;
}

void HTTPClient::setFollowRedirects(followRedirects_t follow)
{
_followRedirects = follow;
}

void HTTPClient::setRedirectLimit(uint16_t limit)
{
_redirectLimit = limit;
}

/**
* set the URL to a new value. Handy for following redirects.
* @param url
*/
bool HTTPClient::setURL(const String& url)
{
// if the new location is only a path then only update the URI
if (url && url[0] == '/') {
_uri = url;
clear();
return true;
}

if (!url.startsWith(_protocol + ':')) {
log_d("new URL not the same protocol, expected '%s', URL: '%s'\n", _protocol.c_str(), url.c_str());
return false;
}

// check if the port is specified
int indexPort = url.indexOf(':', 6); // find the first ':' excluding the one from the protocol
int indexURI = url.indexOf('/', 7); // find where the URI starts to make sure the ':' is not part of it
if (indexPort == -1 || indexPort > indexURI) {
// the port is not specified
_port = (_protocol == "https" ? 443 : 80);
}

// disconnect but preserve _client (clear _canReuse so disconnect will close the connection)
_canReuse = false;
disconnect(true);
return beginInternal(url, _protocol.c_str());
}

const String &HTTPClient::getLocation(void)
{
return _location;
}
27 changes: 27 additions & 0 deletions libraries/HTTPClient/src/HTTPClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ typedef enum {
HTTPC_TE_CHUNKED
} transferEncoding_t;

/**
* redirection follow mode.
* + `HTTPC_DISABLE_FOLLOW_REDIRECTS` - no redirection will be followed.
* + `HTTPC_STRICT_FOLLOW_REDIRECTS` - strict RFC2616, only requests using
* GET or HEAD methods will be redirected (using the same method),
* since the RFC requires end-user confirmation in other cases.
* + `HTTPC_FORCE_FOLLOW_REDIRECTS` - all redirections will be followed,
* regardless of a used method. New request will use the same method,
* and they will include the same body data and the same headers.
* In the sense of the RFC, it's just like every redirection is confirmed.
*/
typedef enum {
HTTPC_DISABLE_FOLLOW_REDIRECTS,
HTTPC_STRICT_FOLLOW_REDIRECTS,
HTTPC_FORCE_FOLLOW_REDIRECTS
} followRedirects_t;


#ifdef HTTPCLIENT_1_1_COMPATIBLE
class TransportTraits;
typedef std::unique_ptr<TransportTraits> TransportTraitsPtr;
Expand Down Expand Up @@ -156,6 +174,11 @@ class HTTPClient
void setConnectTimeout(int32_t connectTimeout);
void setTimeout(uint16_t timeout);

// Redirections
void setFollowRedirects(followRedirects_t follow);
void setRedirectLimit(uint16_t limit); // max redirects to follow for a single request

bool setURL(const String &url);
void useHTTP10(bool usehttp10 = true);

/// request handling
Expand All @@ -182,6 +205,7 @@ class HTTPClient


int getSize(void);
const String &getLocation(void);

WiFiClient& getStream(void);
WiFiClient* getStreamPtr(void);
Expand Down Expand Up @@ -235,6 +259,9 @@ class HTTPClient
int _returnCode = 0;
int _size = -1;
bool _canReuse = false;
followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS;
uint16_t _redirectLimit = 10;
String _location;
transferEncoding_t _transferEncoding = HTTPC_TE_IDENTITY;
};

Expand Down