mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2025-06-04 23:07:27 +02:00
494 lines
21 KiB
C
494 lines
21 KiB
C
|
/***********************************************************************************************************************************
|
||
|
Http Client
|
||
|
***********************************************************************************************************************************/
|
||
|
#include "common/assert.h"
|
||
|
#include "common/debug.h"
|
||
|
#include "common/io/http/client.h"
|
||
|
#include "common/io/http/common.h"
|
||
|
#include "common/io/io.h"
|
||
|
#include "common/io/read.intern.h"
|
||
|
#include "common/io/tls/client.h"
|
||
|
#include "common/log.h"
|
||
|
#include "common/wait.h"
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Http constants
|
||
|
***********************************************************************************************************************************/
|
||
|
#define HTTP_VERSION "HTTP/1.1"
|
||
|
STRING_STATIC(HTTP_VERSION_STR, HTTP_VERSION);
|
||
|
|
||
|
STRING_EXTERN(HTTP_VERB_GET_STR, HTTP_VERB_GET);
|
||
|
|
||
|
#define HTTP_HEADER_CONNECTION "connection"
|
||
|
STRING_STATIC(HTTP_HEADER_CONNECTION_STR, HTTP_HEADER_CONNECTION);
|
||
|
STRING_EXTERN(HTTP_HEADER_CONTENT_LENGTH_STR, HTTP_HEADER_CONTENT_LENGTH);
|
||
|
#define HTTP_HEADER_TRANSFER_ENCODING "transfer-encoding"
|
||
|
STRING_STATIC(HTTP_HEADER_TRANSFER_ENCODING_STR, HTTP_HEADER_TRANSFER_ENCODING);
|
||
|
|
||
|
#define HTTP_VALUE_CONNECTION_CLOSE "close"
|
||
|
STRING_STATIC(HTTP_VALUE_CONNECTION_CLOSE_STR, HTTP_VALUE_CONNECTION_CLOSE);
|
||
|
#define HTTP_VALUE_TRANSFER_ENCODING_CHUNKED "chunked"
|
||
|
STRING_STATIC(HTTP_VALUE_TRANSFER_ENCODING_CHUNKED_STR, HTTP_VALUE_TRANSFER_ENCODING_CHUNKED);
|
||
|
|
||
|
// 5xx errors that should always be retried
|
||
|
#define HTTP_RESPONSE_CODE_RETRY_CLASS 5
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Object type
|
||
|
***********************************************************************************************************************************/
|
||
|
struct HttpClient
|
||
|
{
|
||
|
MemContext *memContext; // Mem context
|
||
|
TimeMSec timeout; // Request timeout
|
||
|
|
||
|
TlsClient *tls; // Tls client
|
||
|
IoRead *ioRead; // Read io interface
|
||
|
|
||
|
unsigned int responseCode; // Response code (e.g. 200, 404)
|
||
|
String *responseMessage; // Response message e.g. (OK, Not Found)
|
||
|
HttpHeader *responseHeader; // Response headers
|
||
|
|
||
|
bool contentChunked; // Is the reponse content chunked?
|
||
|
uint64_t contentSize; // Content size (ignored for chunked)
|
||
|
uint64_t contentRemaining; // Content remaining (per chunk if chunked)
|
||
|
bool closeOnContentEof; // Will server close after content is sent?
|
||
|
bool contentEof; // Has all content been read?
|
||
|
};
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Read content
|
||
|
***********************************************************************************************************************************/
|
||
|
static size_t
|
||
|
httpClientRead(HttpClient *this, Buffer *buffer)
|
||
|
{
|
||
|
FUNCTION_DEBUG_BEGIN(logLevelTrace);
|
||
|
FUNCTION_DEBUG_PARAM(HTTP_CLIENT, this);
|
||
|
FUNCTION_DEBUG_PARAM(BUFFER, buffer);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_ASSERT(buffer != NULL);
|
||
|
FUNCTION_TEST_ASSERT(!bufFull(buffer));
|
||
|
FUNCTION_DEBUG_END();
|
||
|
|
||
|
// Read if EOF has not been reached
|
||
|
size_t actualBytes = 0;
|
||
|
|
||
|
if (!this->contentEof)
|
||
|
{
|
||
|
do
|
||
|
{
|
||
|
// If chunked content and no content remaining
|
||
|
if (this->contentChunked && this->contentRemaining == 0)
|
||
|
{
|
||
|
// Read length of next chunk
|
||
|
MEM_CONTEXT_TEMP_BEGIN()
|
||
|
{
|
||
|
this->contentRemaining = cvtZToUInt64Base(strPtr(strTrim(ioReadLine(tlsClientIoRead(this->tls)))), 16);
|
||
|
}
|
||
|
MEM_CONTEXT_TEMP_END();
|
||
|
|
||
|
// If content remaining is still zero then eof
|
||
|
if (this->contentRemaining == 0)
|
||
|
this->contentEof = true;
|
||
|
}
|
||
|
|
||
|
// Read if there is content remaining
|
||
|
if (this->contentRemaining > 0)
|
||
|
{
|
||
|
// If the buffer is larger than the content that needs to read then limit the buffer size so the read won't block
|
||
|
// or read too far. Casting to size_t is safe on 32-bit because we know the max buffer size is defined as less than
|
||
|
// 2^32 so content remaining can't be more than that.
|
||
|
if (bufRemains(buffer) > this->contentRemaining)
|
||
|
bufLimitSet(buffer, bufSize(buffer) - (bufRemains(buffer) - (size_t)this->contentRemaining));
|
||
|
|
||
|
this->contentRemaining -= ioRead(tlsClientIoRead(this->tls), buffer);
|
||
|
|
||
|
// Clear limit (this works even if the limit was not set and it is easier than checking)
|
||
|
bufLimitClear(buffer);
|
||
|
}
|
||
|
|
||
|
// If no content remaining
|
||
|
if (this->contentRemaining == 0)
|
||
|
{
|
||
|
// If chunked then consume the blank line that follows every chunk. There might be more chunk data so loop back
|
||
|
// around to check.
|
||
|
if (this->contentChunked)
|
||
|
{
|
||
|
ioReadLine(tlsClientIoRead(this->tls));
|
||
|
}
|
||
|
// If total content size was provided then this is eof
|
||
|
else
|
||
|
this->contentEof = true;
|
||
|
}
|
||
|
}
|
||
|
while (!bufFull(buffer) && !this->contentEof);
|
||
|
|
||
|
// If the server notified that it would close the connection after sending content then close the client side
|
||
|
if (this->contentEof && this->closeOnContentEof)
|
||
|
tlsClientClose(this->tls);
|
||
|
}
|
||
|
|
||
|
FUNCTION_DEBUG_RESULT(SIZE, (size_t)actualBytes);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Has all content been read?
|
||
|
***********************************************************************************************************************************/
|
||
|
static bool
|
||
|
httpClientEof(const HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_DEBUG_BEGIN(logLevelTrace);
|
||
|
FUNCTION_DEBUG_PARAM(HTTP_CLIENT, this);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_DEBUG_END();
|
||
|
|
||
|
FUNCTION_DEBUG_RESULT(BOOL, this->contentEof);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
New object
|
||
|
***********************************************************************************************************************************/
|
||
|
HttpClient *
|
||
|
httpClientNew(
|
||
|
const String *host, unsigned int port, TimeMSec timeout, bool verifyPeer, const String *caFile, const String *caPath)
|
||
|
{
|
||
|
FUNCTION_DEBUG_BEGIN(logLevelDebug)
|
||
|
FUNCTION_DEBUG_PARAM(STRING, host);
|
||
|
FUNCTION_DEBUG_PARAM(UINT, port);
|
||
|
FUNCTION_DEBUG_PARAM(TIME_MSEC, timeout);
|
||
|
FUNCTION_DEBUG_PARAM(BOOL, verifyPeer);
|
||
|
FUNCTION_DEBUG_PARAM(STRING, caFile);
|
||
|
FUNCTION_DEBUG_PARAM(STRING, caPath);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(host != NULL);
|
||
|
FUNCTION_DEBUG_END();
|
||
|
|
||
|
HttpClient *this = NULL;
|
||
|
|
||
|
MEM_CONTEXT_NEW_BEGIN("HttpClient")
|
||
|
{
|
||
|
// Allocate state and set context
|
||
|
this = memNew(sizeof(HttpClient));
|
||
|
this->memContext = MEM_CONTEXT_NEW();
|
||
|
|
||
|
this->timeout = timeout;
|
||
|
this->tls = tlsClientNew(host, port, timeout, verifyPeer, caFile, caPath);
|
||
|
}
|
||
|
MEM_CONTEXT_NEW_END();
|
||
|
|
||
|
FUNCTION_DEBUG_RESULT(HTTP_CLIENT, this);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Perform a request
|
||
|
***********************************************************************************************************************************/
|
||
|
Buffer *
|
||
|
httpClientRequest(
|
||
|
HttpClient *this, const String *verb, const String *uri, const HttpQuery *query, const HttpHeader *requestHeader,
|
||
|
bool returnContent)
|
||
|
{
|
||
|
FUNCTION_DEBUG_BEGIN(logLevelDebug)
|
||
|
FUNCTION_DEBUG_PARAM(HTTP_CLIENT, this);
|
||
|
FUNCTION_DEBUG_PARAM(STRING, verb);
|
||
|
FUNCTION_DEBUG_PARAM(STRING, uri);
|
||
|
FUNCTION_DEBUG_PARAM(HTTP_QUERY, query);
|
||
|
FUNCTION_DEBUG_PARAM(HTTP_HEADER, requestHeader);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_ASSERT(verb != NULL);
|
||
|
FUNCTION_TEST_ASSERT(uri != NULL);
|
||
|
FUNCTION_DEBUG_END();
|
||
|
|
||
|
// Buffer for returned content
|
||
|
Buffer *result = NULL;
|
||
|
|
||
|
MEM_CONTEXT_TEMP_BEGIN()
|
||
|
{
|
||
|
bool complete = false;
|
||
|
bool retry;
|
||
|
Wait *wait = this->timeout > 0 ? waitNew(this->timeout) : NULL;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
// Assume there will be no retry
|
||
|
retry = false;
|
||
|
|
||
|
// Free the read interface
|
||
|
ioReadFree(this->ioRead);
|
||
|
this->ioRead = NULL;
|
||
|
|
||
|
// Free response status left over from the last request
|
||
|
httpHeaderFree(this->responseHeader);
|
||
|
this->responseHeader = NULL;
|
||
|
strFree(this->responseMessage);
|
||
|
this->responseMessage = NULL;
|
||
|
|
||
|
// Reset all content info
|
||
|
this->contentChunked = false;
|
||
|
this->contentSize = 0;
|
||
|
this->contentRemaining = 0;
|
||
|
this->closeOnContentEof = false;
|
||
|
this->contentEof = true;
|
||
|
|
||
|
TRY_BEGIN()
|
||
|
{
|
||
|
tlsClientOpen(this->tls);
|
||
|
|
||
|
// Write the request
|
||
|
String *queryStr = httpQueryRender(query);
|
||
|
|
||
|
ioWriteLine(
|
||
|
tlsClientIoWrite(this->tls),
|
||
|
strNewFmt(
|
||
|
"%s %s%s%s " HTTP_VERSION "\r", strPtr(verb), strPtr(httpUriEncode(uri, true)), queryStr == NULL ? "" : "?",
|
||
|
queryStr == NULL ? "" : strPtr(queryStr)));
|
||
|
|
||
|
// Write headers
|
||
|
if (requestHeader != NULL)
|
||
|
{
|
||
|
const StringList *headerList = httpHeaderList(requestHeader);
|
||
|
|
||
|
for (unsigned int headerIdx = 0; headerIdx < strLstSize(headerList); headerIdx++)
|
||
|
{
|
||
|
const String *headerKey = strLstGet(headerList, headerIdx);
|
||
|
ioWriteLine(
|
||
|
tlsClientIoWrite(this->tls),
|
||
|
strNewFmt("%s:%s\r", strPtr(headerKey), strPtr(httpHeaderGet(requestHeader, headerKey))));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Write out blank line and close the write so it flushes
|
||
|
ioWriteLine(tlsClientIoWrite(this->tls), CR_STR);
|
||
|
ioWriteFlush(tlsClientIoWrite(this->tls));
|
||
|
|
||
|
// Read status and make sure it starts with the correct http version
|
||
|
String *status = strTrim(ioReadLine(tlsClientIoRead(this->tls)));
|
||
|
|
||
|
if (!strBeginsWith(status, HTTP_VERSION_STR))
|
||
|
THROW_FMT(FormatError, "http version of response '%s' must be " HTTP_VERSION, strPtr(status));
|
||
|
|
||
|
// Now read the response code and message
|
||
|
status = strSub(status, sizeof(HTTP_VERSION));
|
||
|
|
||
|
int spacePos = strChr(status, ' ');
|
||
|
|
||
|
if (spacePos < 0)
|
||
|
THROW_FMT(FormatError, "response status '%s' must have a space", strPtr(status));
|
||
|
|
||
|
this->responseCode = cvtZToUInt(strPtr(strTrim(strSubN(status, 0, (size_t)spacePos))));
|
||
|
|
||
|
MEM_CONTEXT_BEGIN(this->memContext)
|
||
|
{
|
||
|
this->responseMessage = strSub(status, (size_t)spacePos + 1);
|
||
|
}
|
||
|
MEM_CONTEXT_END();
|
||
|
|
||
|
// Read headers
|
||
|
MEM_CONTEXT_BEGIN(this->memContext)
|
||
|
{
|
||
|
this->responseHeader = httpHeaderNew(NULL);
|
||
|
}
|
||
|
MEM_CONTEXT_END();
|
||
|
|
||
|
do
|
||
|
{
|
||
|
// Read the next header
|
||
|
String *header = strTrim(ioReadLine(tlsClientIoRead(this->tls)));
|
||
|
|
||
|
// If the header is empty then we have reached the end of the headers
|
||
|
if (strSize(header) == 0)
|
||
|
break;
|
||
|
|
||
|
// Split the header and store it
|
||
|
int colonPos = strChr(header, ':');
|
||
|
|
||
|
if (colonPos < 0)
|
||
|
THROW_FMT(FormatError, "header '%s' missing colon", strPtr(strTrim(header)));
|
||
|
|
||
|
String *headerKey = strLower(strTrim(strSubN(header, 0, (size_t)colonPos)));
|
||
|
String *headerValue = strTrim(strSub(header, (size_t)colonPos + 1));
|
||
|
|
||
|
httpHeaderAdd(this->responseHeader, headerKey, headerValue);
|
||
|
|
||
|
// Read transfer encoding (only chunked is supported)
|
||
|
if (strEq(headerKey, HTTP_HEADER_TRANSFER_ENCODING_STR))
|
||
|
{
|
||
|
// Error if transfer encoding is not chunked
|
||
|
if (!strEq(headerValue, HTTP_VALUE_TRANSFER_ENCODING_CHUNKED_STR))
|
||
|
{
|
||
|
THROW_FMT(
|
||
|
FormatError, "only '%s' is supported for '%s' header", HTTP_VALUE_TRANSFER_ENCODING_CHUNKED,
|
||
|
HTTP_HEADER_TRANSFER_ENCODING);
|
||
|
}
|
||
|
|
||
|
this->contentChunked = true;
|
||
|
}
|
||
|
|
||
|
// Read content size
|
||
|
if (strEq(headerKey, HTTP_HEADER_CONTENT_LENGTH_STR))
|
||
|
{
|
||
|
this->contentSize = cvtZToUInt64(strPtr(headerValue));
|
||
|
this->contentRemaining = this->contentSize;
|
||
|
}
|
||
|
|
||
|
// If the server notified of a closed connection then close the client connection after reading content. This
|
||
|
// prevents doing a retry on the next request when using the closed connection.
|
||
|
if (strEq(headerKey, HTTP_HEADER_CONNECTION_STR) && strEq(headerValue, HTTP_VALUE_CONNECTION_CLOSE_STR))
|
||
|
this->closeOnContentEof = true;
|
||
|
}
|
||
|
while (1);
|
||
|
|
||
|
// Error if transfer encoding and content length are both set
|
||
|
if (this->contentChunked && this->contentSize > 0)
|
||
|
{
|
||
|
THROW_FMT(
|
||
|
FormatError, "'%s' and '%s' headers are both set", HTTP_HEADER_TRANSFER_ENCODING,
|
||
|
HTTP_HEADER_CONTENT_LENGTH);
|
||
|
}
|
||
|
|
||
|
// If content chunked or content length > 0 then there is content to read
|
||
|
if (this->contentChunked || this->contentSize > 0)
|
||
|
{
|
||
|
this->contentEof = false;
|
||
|
|
||
|
// If all content should be returned from this function then read the buffer. Also read the reponse if there
|
||
|
// has been an error.
|
||
|
if (returnContent || httpClientResponseCode(this) != 200)
|
||
|
{
|
||
|
result = bufNew(0);
|
||
|
|
||
|
do
|
||
|
{
|
||
|
bufResize(result, bufSize(result) + ioBufferSize());
|
||
|
httpClientRead(this, result);
|
||
|
}
|
||
|
while (!httpClientEof(this));
|
||
|
}
|
||
|
// Else create the read interface
|
||
|
else
|
||
|
{
|
||
|
MEM_CONTEXT_BEGIN(this->memContext)
|
||
|
{
|
||
|
this->ioRead = ioReadNewP(
|
||
|
this, .eof = (IoReadInterfaceEof)httpClientEof, .read = (IoReadInterfaceRead)httpClientRead);
|
||
|
ioReadOpen(this->ioRead);
|
||
|
}
|
||
|
MEM_CONTEXT_END();
|
||
|
}
|
||
|
}
|
||
|
// If the server notified that it would close the connection after sending content then close the client side
|
||
|
else if (this->closeOnContentEof)
|
||
|
tlsClientClose(this->tls);
|
||
|
|
||
|
// Retry when reponse code is 5xx. These errors generally represent a server error for a request that looks valid.
|
||
|
// There are a few errors that might be permanently fatal but they are rare and it seems best not to try and pick
|
||
|
// and choose errors in this class to retry.
|
||
|
if (httpClientResponseCode(this) / 100 == HTTP_RESPONSE_CODE_RETRY_CLASS)
|
||
|
THROW_FMT(ServiceError, "[%u] %s", httpClientResponseCode(this), strPtr(httpClientResponseMessage(this)));
|
||
|
|
||
|
// Request was successful
|
||
|
complete = true;
|
||
|
}
|
||
|
CATCH_ANY()
|
||
|
{
|
||
|
// Retry if wait time has not expired
|
||
|
if (wait != NULL && waitMore(wait))
|
||
|
{
|
||
|
LOG_DEBUG("retry %s: %s", errorTypeName(errorType()), errorMessage());
|
||
|
retry = true;
|
||
|
}
|
||
|
|
||
|
tlsClientClose(this->tls);
|
||
|
}
|
||
|
TRY_END();
|
||
|
}
|
||
|
while (!complete && retry);
|
||
|
|
||
|
if (!complete)
|
||
|
RETHROW();
|
||
|
|
||
|
// Move the result buffer (if any) to the parent context
|
||
|
bufMove(result, MEM_CONTEXT_OLD());
|
||
|
}
|
||
|
MEM_CONTEXT_TEMP_END();
|
||
|
|
||
|
FUNCTION_DEBUG_RESULT(BUFFER, result);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Get read interface
|
||
|
***********************************************************************************************************************************/
|
||
|
IoRead *
|
||
|
httpClientIoRead(const HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_TEST_BEGIN();
|
||
|
FUNCTION_TEST_PARAM(HTTP_CLIENT, this);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_END();
|
||
|
|
||
|
FUNCTION_TEST_RESULT(IO_READ, this->ioRead);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Get the response code
|
||
|
***********************************************************************************************************************************/
|
||
|
unsigned int
|
||
|
httpClientResponseCode(const HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_TEST_BEGIN();
|
||
|
FUNCTION_TEST_PARAM(HTTP_CLIENT, this);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_END();
|
||
|
|
||
|
FUNCTION_TEST_RESULT(UINT, this->responseCode);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Get the response headers
|
||
|
***********************************************************************************************************************************/
|
||
|
const HttpHeader *
|
||
|
httpClientReponseHeader(const HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_TEST_BEGIN();
|
||
|
FUNCTION_TEST_PARAM(HTTP_CLIENT, this);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_END();
|
||
|
|
||
|
FUNCTION_TEST_RESULT(HTTP_HEADER, this->responseHeader);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Get the response message
|
||
|
***********************************************************************************************************************************/
|
||
|
const String *
|
||
|
httpClientResponseMessage(const HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_TEST_BEGIN();
|
||
|
FUNCTION_TEST_PARAM(HTTP_CLIENT, this);
|
||
|
|
||
|
FUNCTION_TEST_ASSERT(this != NULL);
|
||
|
FUNCTION_TEST_END();
|
||
|
|
||
|
FUNCTION_TEST_RESULT(STRING, this->responseMessage);
|
||
|
}
|
||
|
|
||
|
/***********************************************************************************************************************************
|
||
|
Free the object
|
||
|
***********************************************************************************************************************************/
|
||
|
void
|
||
|
httpClientFree(HttpClient *this)
|
||
|
{
|
||
|
FUNCTION_TEST_BEGIN();
|
||
|
FUNCTION_TEST_PARAM(HTTP_CLIENT, this);
|
||
|
FUNCTION_TEST_END();
|
||
|
|
||
|
if (this != NULL)
|
||
|
memContextFree(this->memContext);
|
||
|
|
||
|
FUNCTION_TEST_RESULT_VOID();
|
||
|
}
|