Kawe Mazidjatari b3a68ed095 Add EABase, EAThread and DirtySDK to R5sdk
DirtySDK (EA's Dirty Sockets library) will be used for the LiveAPI implementation, and depends on: EABase, EAThread.
2024-04-05 18:29:03 +02:00

3759 lines
142 KiB
C

/*H********************************************************************************/
/*!
\File protohttp2.c
\Description
This module implements an HTTP/2 client that can perform basic transactions
with an HTTP/2 server. It conforms to but does not fully implement
the 2.0 HTTP spec (https://tools.ietf.org/html/rfc7540), and
allows for secure HTTP transactions as well as insecure transactions.
\Copyright
Copyright (c) Electronic Arts 2016-2018. ALL RIGHTS RESERVED.
\Todo
Implement HTTP/2 proxy support (via CONNECT) when we find a proxy that supports
it.
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************H*/
/*** Include files ****************************************************************/
#include <string.h>
#include <stdlib.h>
#include "DirtySDK/dirtydefs.h"
#include "DirtySDK/dirtysock/dirtyerr.h"
#include "DirtySDK/dirtysock/dirtylib.h"
#include "DirtySDK/dirtysock/dirtymem.h"
#include "DirtySDK/dirtysock/dirtynet.h"
#include "DirtySDK/dirtyvers.h"
#include "DirtySDK/proto/protossl.h"
#include "DirtySDK/util/hpack.h"
#include "DirtySDK/proto/protohttp2.h"
/*** Defines **********************************************************************/
//! size of a http2 header (24-bit length, 8-bit flag, 8-bit type and 31-bit stream id)
#define PROTOHTTP2_HEADER_SIZE (9)
//! size of http2 setting (16-bit key and 32-bit value)
#define PROTOHTTP2_SETTING_SIZE (6)
//! size of http2 ping (8 octets of opaque data)
#define PROTOHTTP2_PING_SIZE (8)
//! size of http2 goaway (31-bit stream id and 32-bit error code)
#define PROTOHTTP2_GOAWAY_SIZE (8)
//! size of http2 rst_stream (32-bit error code)
#define PROTOHTTP2_RST_STREAM_SIZE (4)
//! size of http2 window update (31-bit window size increment)
#define PROTOHTTP2_WINDOW_UPDATE_SIZE (4)
//! size of the http2 priority frame (1 bit exclusive, 31-bit stream dependency, 8-bit weight)
#define PROTOHTTP2_PRIORITY_SIZE (5)
//! reserved size in the buffer for a header + important frames (ping/settings/rst_stream)
#define PROTOHTTP2_RESERVED_SIZE \
((PROTOHTTP2_HEADER_SIZE*4)+(PROTOHTTP2_SETTING_SIZE*SETTINGS_NUM)+PROTOHTTP2_PING_SIZE+PROTOHTTP2_RST_STREAM_SIZE)
// flags for http frames
#define PROTOHTTP2_FLAG_ACK (1 << 0) //!< acknowledge ping / settings
#define PROTOHTTP2_FLAG_END_STREAM (1 << 0) //!< indicates end data chunk
#define PROTOHTTP2_FLAG_END_HEADERS (1 << 2) //!< indicates end headers, ie: no continuation frames
#define PROTOHTTP2_FLAG_PADDED (1 << 3) //!< indicates data / header is padded so look for pad length
#define PROTOHTTP2_FLAG_PRIORITY (1 << 5) //!< indicates priority information
//! size of sent/received header cache
#define PROTOHTTP2_HDRCACHESIZE (1024*2)
//! size of the receive window
#define PROTOHTTP2_WINDOWSIZE (1024*16)
//! module version (maj.min)
#define PROTOHTTP2_VERSION (0x0200)
//! maximum number of concurrent streams supported
#define PROTOHTTP2_MAX_STREAMS (100)
//! default protocol timeout
#define PROTOHTTP2_TIMEOUT_DEFAULT (30*1000)
//! minimum frame size
#define PROTOHTTP2_FRAMESIZE_MIN (1 << 14)
//! maximum frame size
#define PROTOHTTP2_FRAMESIZE_MAX ((1 << 24) - 1)
//! maximum flow-control window size
#define PROTOHTTP2_WINDOWSIZE_MAX ((1u << 31) - 1)
/*** Macros ***********************************************************************/
//! calculates how much free space we have for encoding data/header frames (ie: user frames)
#define PROTOHTTP2_CalculateFreeSpace(pState) ((pState)->iOutMax-(pState)->iOutLen-PROTOHTTP2_RESERVED_SIZE)
//! clamps the maximum frame size (16k-16m)
#define PROTOHTTP2_ClampFrameSize(iValue) (DS_CLAMP((iValue), PROTOHTTP2_FRAMESIZE_MIN, PROTOHTTP2_FRAMESIZE_MAX))
#if DIRTYCODE_LOGGING
// helper macro to print the frame type
#define PROTOHTTP2_GetFrameTypeStr(uType) ((uType) <= FRAMETYPE_LAST ? _ProtoHttp2_strFrameType[(uType)] : "UNKNOWN")
#endif
/*** Type Definitions *************************************************************/
//! settings supported on the http2 connection
enum
{
SETTINGS_HEADER_TABLE_SIZE = 1, //!< the maximum size of the header compression table used to decode header blocks
SETTINGS_ENABLE_PUSH, //!< used to disable server push
SETTINGS_MAX_CONCURRENT_STREAM, //!< the maximum number of concurrent streams that the sender will allow
SETTINGS_INITIAL_WINDOW_SIZE, //!< the sender's initial window size (in octets) for stream-level flow control
SETTINGS_MAX_FRAME_SIZE, //!< the size of the largest frame payload that the sender is willing to receive
SETTINGS_MAX_HEADER_LIST_SIZE, //!< the maximum size of header list that the sender is prepared to accept
SETTINGS_NUM = SETTINGS_MAX_HEADER_LIST_SIZE
};
//! the different types of http2 frames we expect
typedef enum FrameTypeE
{
FRAMETYPE_DATA, //!< convey arbitrary, variable-length sequences of octets associated with a stream
FRAMETYPE_HEADERS, //!< is used to open a stream, and additionally carries a header block fragment
FRAMETYPE_PRIORITY, //!< specifies the sender-advised priority of a stream
FRAMETYPE_RST_STREAM, //!< allows for immediate termination of a stream
FRAMETYPE_SETTINGS, //!< conveys configuration parameters that affect how endpoints communicate
FRAMETYPE_PUSH_PROMISE, //!< used to notify the peer endpoint in advance of streams the sender intends to initiate
FRAMETYPE_PING, //!< for measuring a minimal RTT from the sender, as well as determining whether an idle connection is still functional
FRAMETYPE_GOAWAY, //!< is used to initiate shutdown of a connection or to signal serious error conditions
FRAMETYPE_WINDOW_UPDATE,//!< used to implement flow control
FRAMETYPE_CONTINUATION, //!< used to continue a sequence of header block fragments
FRAMETYPE_LAST = FRAMETYPE_CONTINUATION
} FrameTypeE;
//! the different types of errors we send in RST_STREAM/GOAWAY frames
typedef enum ErrorTypeE
{
ERRORTYPE_NO_ERROR, //!< graceful shutdown
ERRORTYPE_PROTOCOL_ERROR, //!< protocol error detected
ERRORTYPE_INTERNAL_ERROR, //!< implementation fault
ERRORTYPE_FLOW_CONTROL_ERROR, //!< flow-control limits exceeded
ERRORTYPE_SETTINGS_TIMEOUT, //!< settings not acknowledged
ERRORTYPE_STREAM_CLOSED, //!< frame received for closed stream
ERRORTYPE_FRAME_SIZE_ERROR, //!< frame size incorrect
ERRORTYPE_REFUSED_STREAM, //!< stream not processed
ERRORTYPE_CANCEL, //!< stream cancelled
ERRORTYPE_COMPRESSION_ERROR, //!< compression state not updated
ERRORTYPE_CONNECT_ERROR, //!< TCP connection error for CONNECT method
ERRORTYPE_ENHANCE_YOUR_CALM, //!< processing capacity exceeded
ERRORTYPE_INADEQUATE_SECURITY, //!< negotiated TLS parameters not acceptable
ERRORTYPE_HTTP_1_1_REQUIRED //!< use HTTP/1.1 for the request
} ErrorTypeE;
//! header embedded into every http2 frame
typedef struct FrameHeaderT
{
uint32_t uLength; //!< length of the payload (24 bits)
uint8_t uType; //!< the frame type corresponds to the FrameTypeE enum (8 bits)
uint8_t uFlags; //!< the frame flags, this value depends on the frame type (8 bits)
uint8_t uPadding; //!< amount of padding (8 bits)
uint8_t bSkipFrame;//!< used for receive processing, not sent on the wire
int32_t iStreamId; //!< identifier of the stream (31 bits)
} FrameHeaderT;
//! settings information
typedef struct SettingsT
{
uint32_t uHeaderTableSize; //!< SETTINGS_HEADER_TABLE_SIZE
uint32_t uEnablePush; //!< SETTINGS_ENABLE_PUSH
uint32_t uMaxConcurrentStream; //!< SETTINGS_MAX_CONCURRENT_STREAM
uint32_t uInitialWindowSize; //!< SETTINGS_INITIAL_WINDOW_SIZE
uint32_t uMaxFrameSize; //!< SETTINGS_MAX_FRAME_SIZE
uint32_t uMaxHeaderListSize; //!< SETTINGS_MAX_HEADER_LIST_SIZE
} SettingsT;
//! stream information
typedef struct StreamInfoT
{
int32_t iStreamId; //!< stream identifier
ProtoHttp2StreamStateE eState; //!< current state of the stream
ProtoHttpRequestTypeE eRequestType; //!< request type of current request
ProtoHttpResponseE eResponseCode; //!< HTTP error code from the response
ErrorTypeE eErrorType; //!< cached error code from a rst_stream
char *pHeader; //!< storage for response header (uncompressed)
int32_t iHeaderLen; //!< length of the response header
char strRequestHeader[PROTOHTTP2_HDRCACHESIZE]; //!< storage for cached request header (uncompressed)
uint8_t *pData; //!< storage for data frames used for polling
int32_t iDataLen; //!< length of the data frame storage
int32_t iDataMax; //!< maximum size of the data frame storage
int32_t iLocalWindow; //!< size of the stream wide receive window (local)
int32_t iPeerWindow; //!< size of the stream wide receive window (remote)
ProtoHttp2WriteCbT *pWriteCb; //!< user write callback pointer
ProtoHttp2CustomHeaderCbT *pCustomHeaderCb; //!< custom header callback pointer
ProtoHttp2ReceiveHeaderCbT *pReceiveHeaderCb; //!< receive header callback pointer
void *pUserData; //!< user data to pass along with callback
int64_t iBodySize; //!< size of body data
int64_t iBodyReceived; //!< size of body data received by caller
int32_t iRecvSize; //!< amount of data received by ProtoHttp2RecvAll
int32_t iHdrDate; //!< last modified date
} StreamInfoT;
//! http2 module state
struct ProtoHttp2RefT
{
ProtoSSLRefT *pSsl; //!< ssl module used for communication
//! memgroup data
int32_t iMemGroup;
void *pMemGroupUserData;
uint8_t *pOutBuf; //!< send buffer
int32_t iOutMax; //!< size of send buffer
int32_t iOutLen; //!< length in send buffer
int32_t iOutOff; //!< offset into the send buffer
uint8_t *pInpBuf; //!< recv buffer
int32_t iInpMax; //!< size of recv buffer
int32_t iInpLen; //!< length in recv buffer
int32_t iLocalWindow; //!< size of the connection wide receive window (local)
int32_t iPeerWindow; //!< size of the connection wide receive window (remote)
//! callback data
ProtoHttp2CustomHeaderCbT *pCustomHeaderCb;
ProtoHttp2ReceiveHeaderCbT *pReceiveHeaderCb;
void *pCallbackRef;
int32_t iStreamId; //!< identifier of the current stream id for new requests
int32_t iVerbose; //!< logging verbosity
int32_t iSslFail; //!< ssl failure code, if any
int32_t iHresult; //!< ssl hresult code, if any
uint32_t uSettingsTimer; //!< timeout for the peer to acknowledge our settings
uint32_t uPingTimer; //!< timeout for the peer to acknowledge our ping
uint32_t uTimer; //!< timeout timer
uint32_t uTimeout; //!< protocol timeout
NetCritT HttpCrit; //!< critical section for guarding update from send/recv
//! settings based on ProtoHttp2SettingsE
SettingsT PeerSettings; //!< settings advertised by our peer
SettingsT TempSettings; //!< settings we will advertise to our peer
SettingsT LocalSettings; //!< settings we have advertised and were ack'd
char strHost[256]; //!< server name
char strBaseHost[256]; //!< base server name (used for partial urls)
int32_t iPort; //!< server port
int32_t bSecure; //!< secure connection?
int32_t iBasePort; //!< base port (used for partial urls)
int32_t bBaseSecure; //!< base security settings (used for partial urls)
enum
{
ST_IDLE, //!< default state
ST_CONN, //!< connecting to the server
ST_ACTIVE, //!< active connection
ST_FAIL //!< connection failure
} eState; //!< current state of the module
ErrorTypeE eErrorType; //!< cached error when handling peer frames (goaway related)
HpackRefT *pEncoder; //!< encoder context
HpackRefT *pDecoder; //!< decoder context
uint8_t bHuffman; //!< use huffman encoding? (default=FALSE)
uint8_t bSettingsRecv; //!< have we received any settings from our peer?
uint8_t bTimeout; //!< timeout indicator
uint8_t _pad;
char *pAppendHdr; //!< append header ('apnd' control) buffer
int32_t iAppendLen; //!< size of append header
int32_t iNumStreams; //!< current number of active streams
StreamInfoT Streams[PROTOHTTP2_MAX_STREAMS]; //!< list of stream info
};
/*** Variables ********************************************************************/
//! used when establishing a connection to try to cause failure with non http2 endpoints
static const uint8_t _ProtoHttp2_ConnectionPreface[24] =
{
0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54,
0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a,
0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a
};
#if DIRTYCODE_LOGGING
//! frame types for logging purposes
static const char *_ProtoHttp2_strFrameType[] =
{
"DATA",
"HEADERS",
"PRIORITY",
"RST_STREAM",
"SETTINGS",
"PUSH_PROMISE",
"PING",
"GOAWAY",
"WINDOW_UPDATE",
"CONTINUATION"
};
//! settings names for logging purposes
static const char *_ProtoHttp2_strSettingName[SETTINGS_NUM] =
{
"SETTINGS_HEADER_TABLE_SIZE",
"SETTINGS_ENABLE_PUSH",
"SETTINGS_MAX_CONCURRENT_STREAM",
"SETTINGS_INITIAL_WINDOW_SIZE",
"SETTINGS_MAX_FRAME_SIZE",
"SETTINGS_MAX_HEADER_LIST_SIZE"
};
//! protohttp2 state for logging
static const char *_ProtoHttp2_strState[] =
{
"ST_IDLE",
"ST_CONN",
"ST_ACTIVE",
"ST_FAIL"
};
#endif
//! error types to string mapping
static const char *_ProtoHttp2_strErrorType[] =
{
"NO_ERROR",
"PROTOCOL_ERROR",
"INTERNAL_ERROR",
"FLOW_CONTROL_ERROR",
"SETTINGS_TIMEOUT",
"STREAM_CLOSED",
"FRAME_SIZE_ERROR",
"REFUSED_STREAM",
"CANCEL",
"COMPRESSION_ERROR",
"CONNECT_ERROR",
"ENHANCE_YOUR_CALM",
"INADEQUATE_SECURITY",
"HTTP_1_1_REQUIRED"
};
//! default settings based on rfc
static const SettingsT _ProtoHttp2_DefaultSettings =
{
0x1000, //!< SETTINGS_HEADER_TABLE_SIZE
0x1, //!< SETTINGS_ENABLE_PUSH
0x7fffffff, //!< SETTINGS_MAX_CONCURRENT_STREAM
0xffff, //!< SETTINGS_INITIAL_WINDOW_SIZE
PROTOHTTP2_FRAMESIZE_MIN, //!< SETTINGS_MAX_FRAME_SIZE
0x7fffffff //!< SETTINGS_MAX_HEADER_LIST_SIZE
};
//! http request names
static const char *_ProtoHttp2_strRequestNames[PROTOHTTP_NUMREQUESTTYPES] =
{
"HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "CONNECT"
};
/*** Private Functions ************************************************************/
static void _ProtoHttp2StreamInfoCleanup(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo);
static void _ProtoHttp2PrepareRstStream(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, ErrorTypeE eErrorType, const char *pMessage);
/*F********************************************************************************/
/*!
\Function _ProtoHttp2Close
\Description
Closes the connection to the peer
\Input *pState - module state
\Input *pReason - string representation of why we are closing the connection
\Version 09/28/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2Close(ProtoHttp2RefT *pState, const char *pReason)
{
if (ProtoSSLStat(pState->pSsl, 'stat', NULL, 0) >= 0)
{
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] closing connection: %s\n", pState, pReason));
ProtoSSLDisconnect(pState->pSsl);
pState->eState = ST_FAIL;
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2StreamCloseOnError
\Description
Sets the stream state to closed and fires a failure write callback
\Input *pState - module state
\Input *pStreamInfo - stream information we are updating
\Version 01/05/2017 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2StreamCloseOnError(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo)
{
// if the stream already closed nothing left to do
if (pStreamInfo->eState == STREAMSTATE_CLOSED)
{
return;
}
pStreamInfo->eState = STREAMSTATE_CLOSED;
// if we have a write callback, let the user know that we had an error occur
if (pStreamInfo->pWriteCb != NULL)
{
ProtoHttp2WriteCbInfoT CbInfo;
CbInfo.iStreamId = pStreamInfo->iStreamId;
CbInfo.eRequestType = pStreamInfo->eRequestType;
CbInfo.eRequestResponse = pStreamInfo->eResponseCode;
pStreamInfo->pWriteCb(pState, &CbInfo, NULL, pState->bTimeout ? PROTOHTTP2_TIMEOUT : PROTOHTTP2_RECVFAIL, pStreamInfo->pUserData);
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2Send
\Description
Try and send some data
\Input *pState - reference pointer
\Input *pBuf - pointer to buffer to send from
\Input iBufSize - amount of data to try and send
\Output
int32_t - negative=error, else number of bytes sent
\Version 11/02/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2Send(ProtoHttp2RefT *pState, const uint8_t *pBuf, int32_t iBufSize)
{
int32_t iResult, iStream;
// try and send some data
iResult = ProtoSSLSend(pState->pSsl, (const char *)pBuf, iBufSize);
if (iResult > 0)
{
#if DIRTYCODE_LOGGING
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] sent %d bytes\n", pState, iResult));
if (pState->iVerbose > 3)
{
NetPrintMem(pBuf, iResult, "http2-send");
}
#endif
pState->uTimer = NetTick() + pState->uTimeout;
}
else if (iResult < 0)
{
NetPrintf(("protohttp2: [%p] error %d sending %d bytes\n", pState, iResult, iBufSize));
pState->eState = ST_FAIL;
pState->iSslFail = ProtoSSLStat(pState->pSsl, 'fail', NULL, 0);
pState->iHresult = ProtoSSLStat(pState->pSsl, 'hres', NULL, 0);
// close all the streams that are currently active
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamCloseOnError(pState, &pState->Streams[iStream]);
}
}
return(iResult);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2Recv
\Description
Try and recv some data
\Input *pState - reference pointer
\Input *pBuf - pointer to buffer to recv to
\Input iBufSize - amount of data we can accept
\Output
int32_t - negative=error, else number of bytes recv
\Version 11/02/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2Recv(ProtoHttp2RefT *pState, uint8_t *pBuf, int32_t iBufSize)
{
int32_t iResult, iStream;
// try and send some data
iResult = ProtoSSLRecv(pState->pSsl, (char *)pBuf, iBufSize);
if (iResult > 0)
{
#if DIRTYCODE_LOGGING
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] recv %d bytes\n", pState, iResult));
if (pState->iVerbose > 3)
{
NetPrintMem(pBuf, iResult, "http2-recv");
}
#endif
pState->uTimer = NetTick() + pState->uTimeout;
}
else if (iResult < 0)
{
NetPrintf(("protohttp2: [%p] %s got ST_FAIL (err=%d)\n", pState, _ProtoHttp2_strState[pState->eState], iResult));
pState->eState = ST_FAIL;
pState->iSslFail = ProtoSSLStat(pState->pSsl, 'fail', NULL, 0);
pState->iHresult = ProtoSSLStat(pState->pSsl, 'hres', NULL, 0);
// close all the streams that are currently active
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamCloseOnError(pState, &pState->Streams[iStream]);
}
}
return(iResult);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2ApplyBaseUrl
\Description
Apply base url elements (if set) to any url elements not specified (relative
url support).
\Input *pState - module state
\Input *pKind - parsed http kind ("http" or "https")
\Input *pHost - [in/out] parsed URL host
\Input iHostSize - size of pHost buffer
\Input *pPort - [in/out] parsed port
\Input *pSecure - [in/out] parsed security (0 or 1)
\Input bPortSpecified - TRUE if a port is explicitly specified in the url, else FALSE
\Output
uint8_t - non-zero if changed, else zero
\Version 02/03/2010 (jbrookes)
*/
/********************************************************************************F*/
static uint8_t _ProtoHttp2ApplyBaseUrl(ProtoHttp2RefT *pState, const char *pKind, char *pHost, int32_t iHostSize, int32_t *pPort, int32_t *pSecure, uint8_t bPortSpecified)
{
uint8_t bChanged = FALSE;
if ((*pHost == '\0') && (pState->strBaseHost[0] != '\0'))
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] host not present; setting to %s\n", pState, pState->strBaseHost));
ds_strnzcpy(pHost, pState->strBaseHost, iHostSize);
bChanged = TRUE;
}
if ((bPortSpecified == FALSE) && (pState->iBasePort != 0))
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] port not present; setting to %d\n", pState, pState->iBasePort));
*pPort = pState->iBasePort;
bChanged = TRUE;
}
if (*pKind == '\0')
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] kind (protocol) not present; setting to %d\n", pState, pState->bBaseSecure));
*pSecure = pState->bBaseSecure;
// if our port setting is default and incompatible with our security setting, override it
if (((*pPort == 80) && (*pSecure == TRUE)) || ((*pPort == 443) && (*pSecure == FALSE)))
{
*pPort = *pSecure ? 443 : 80;
}
bChanged = TRUE;
}
return(bChanged);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2Reset
\Description
Reset the internal state to before we connected to a peer
\Input *pState - module state
\Version 10/31/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2Reset(ProtoHttp2RefT *pState)
{
int32_t iStream;
// set peer's default setting values (these are overwritten on connection establishment)
ds_memcpy(&pState->PeerSettings, &_ProtoHttp2_DefaultSettings, sizeof(pState->PeerSettings));
// set my local default setting values
ds_memcpy(&pState->LocalSettings, &_ProtoHttp2_DefaultSettings, sizeof(pState->LocalSettings));
// set my updated local settings that we will advertise during connection establishment
ds_memcpy(&pState->TempSettings, &_ProtoHttp2_DefaultSettings, sizeof(pState->TempSettings));
pState->TempSettings.uEnablePush = 0; // tell the peer that it cannot push
/* tell peer the size of header we are willing to support (uncompressed)
note: if the size is larger than the size of max header list you will
need to look into supporting continuation frames */
pState->TempSettings.uMaxHeaderListSize = PROTOHTTP2_HDRCACHESIZE;
// update the size of the recv window
pState->iLocalWindow = pState->TempSettings.uInitialWindowSize = PROTOHTTP2_WINDOWSIZE;
pState->iPeerWindow = pState->PeerSettings.uInitialWindowSize;
// update the maximum frame size based on our input buffer
if (pState->TempSettings.uMaxFrameSize < (uint32_t)(pState->iInpMax-PROTOHTTP2_RESERVED_SIZE))
{
pState->TempSettings.uMaxFrameSize = (uint32_t)(pState->iInpMax-PROTOHTTP2_RESERVED_SIZE);
}
// clear the dynamic tables
HpackClear(pState->pDecoder);
HpackClear(pState->pEncoder);
// set the stream id to 1 (initial state)
pState->iStreamId = 1;
// clean up the number of tracked streams
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamInfoCleanup(pState, &pState->Streams[iStream]);
}
pState->iNumStreams = 0;
// set the default state
pState->eState = ST_IDLE;
pState->eErrorType = ERRORTYPE_NO_ERROR;
pState->bTimeout = FALSE;
// reset the sizes of the buffers
pState->iOutLen = pState->iInpLen = 0;
// reset the offset
pState->iOutOff = 0;
// reset variables dealing with settings
pState->bSettingsRecv = FALSE;
pState->uSettingsTimer = 0;
// reset ping timer
pState->uPingTimer = 0;
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2StreamInfoGet
\Description
Retrieves the stream information by identifier
\Input *pState - module state
\Input iStreamId - identifier used for the lookup
\Output
StreamInfoT * - pointer to stream information or NULL if not found
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static StreamInfoT *_ProtoHttp2StreamInfoGet(ProtoHttp2RefT *pState, int32_t iStreamId)
{
StreamInfoT *pStreamInfo = NULL;
int32_t iStream;
// iterate through stream info and look for a match
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
if (pState->Streams[iStream].iStreamId == iStreamId)
{
pStreamInfo = &pState->Streams[iStream];
break;
}
}
// return info to caller, or NULL if no match
return(pStreamInfo);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2StreamInfoCleanup
\Description
Cleanup any dynamic memory consumed by the stream info struct
\Input *pState - module state
\Input *pStreamInfo - pointer to the structure needing cleanup
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2StreamInfoCleanup(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo)
{
if (pStreamInfo->pHeader != NULL)
{
DirtyMemFree(pStreamInfo->pHeader, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
pStreamInfo->pHeader = NULL;
}
if (pStreamInfo->pData != NULL)
{
DirtyMemFree(pStreamInfo->pData, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
pStreamInfo->pData = NULL;
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2DecodeFrameHeader
\Description
Decodes the header of a http2 frame
\Input *pState - module state
\Input *pBuf - the buffer we are pasing the data out of
\Input iBufLen - the size of the buffer
\Input *pHeader - [out] location we are parsing into
\Input **pStreamInfo- [out] stream info for frames that include non-zero streams
\Output
const uint8_t * - the new location of the buffer after parsing or NULL
\Version 09/28/2016 (eesponda)
*/
/********************************************************************************F*/
static const uint8_t *_ProtoHttp2DecodeFrameHeader(ProtoHttp2RefT *pState, const uint8_t *pBuf, int32_t iBufLen, FrameHeaderT *pHeader, StreamInfoT **pStreamInfo)
{
// make sure we can parse the whole header
if (iBufLen < PROTOHTTP2_HEADER_SIZE)
{
return(NULL);
}
// make sure that we in a valid state, in the case that a previous frame in the current receive caused an error
if (pState->eErrorType != ERRORTYPE_NO_ERROR)
{
return(NULL);
}
// decode 24-bit length
pHeader->uLength = *pBuf++ << 16;
pHeader->uLength |= *pBuf++ << 8;
pHeader->uLength |= *pBuf++;
// decode 8-bit type
pHeader->uType = *pBuf++;
// decode 8-bit flags
pHeader->uFlags = *pBuf++;
// decode 31-bit stream id (ignoring the high bit)
pHeader->iStreamId = (*pBuf++ & 0x7f) << 24;
pHeader->iStreamId |= *pBuf++ << 16;
pHeader->iStreamId |= *pBuf++ << 8;
pHeader->iStreamId |= *pBuf++;
// set the skip frame to false by default
pHeader->bSkipFrame = FALSE;
/* ref: https://tools.ietf.org/html/rfc7540#section-5.1.1
An endpoint that receives an unexpected stream identifier MUST respond with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if ((pHeader->iStreamId > 0) && ((*pStreamInfo = _ProtoHttp2StreamInfoGet(pState, pHeader->iStreamId)) == NULL))
{
/* ignore frames that are associated to streams that are no longer tracked. the rfc has more detailed rules, which can be tricky to implement.
just ignoring should cover our bases since this is an unexpected case. */
if (pHeader->iStreamId < pState->iStreamId)
{
pHeader->bSkipFrame = TRUE;
}
else
{
NetPrintf(("protohttp2: [%p] received unrecogized stream identifier (0x%08x), closing connection\n", pState, pHeader->iStreamId));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return(NULL);
}
}
/* ref: https://tools.ietf.org/html/rfc7540#section-4.2
An endpoint MUST send an error code of FRAME_SIZE_ERROR if a frame exceeds the size defined in SETTINGS_MAX_FRAME_SIZE.
A frame size error in a frame that could alter the state of the entire connection MUST be treated as a connection error (Section 5.4.1); this includes any frame
carrying a header block (Section 4.3) (that is, HEADERS, PUSH_PROMISE, and CONTINUATION), SETTINGS, and any frame with a stream identifier of 0. */
if (pHeader->uLength > pState->LocalSettings.uMaxFrameSize)
{
if ((pHeader->iStreamId == 0) || (pHeader->uType == FRAMETYPE_HEADERS) || (pHeader->uType == FRAMETYPE_PUSH_PROMISE) || (pHeader->uType == FRAMETYPE_CONTINUATION))
{
NetPrintf(("protohttp2: [%p] received frame with size exceeding maximum on connection impacting frame, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
return(NULL);
}
else
{
_ProtoHttp2PrepareRstStream(pState, *pStreamInfo, ERRORTYPE_FRAME_SIZE_ERROR, "received frame header with invalid size");
pHeader->bSkipFrame = TRUE;
return(NULL);
}
}
// make sure the whole frame has arrived, since anything past this we assume that we have received more than the PROTOHTTP2_HEADER_SIZE
if ((iBufLen-PROTOHTTP2_HEADER_SIZE) < (int32_t)pHeader->uLength)
{
return(NULL);
}
/* ref: https://tools.ietf.org/html/rfc7540#section-4.1
Flags that have no defined semantics for a particular frame type MUST be ignored and MUST be left unset (0x0) when sending. */
if (((pHeader->uType == FRAMETYPE_HEADERS) || (pHeader->uType == FRAMETYPE_DATA) || (pHeader->uType == FRAMETYPE_PUSH_PROMISE)) && ((pHeader->uFlags & PROTOHTTP2_FLAG_PADDED) != 0))
{
/* decode 8-bit pad length if present, add 1 for the length field itself; this is technically part of the frame payload but we do it here at a
central location */
pHeader->uPadding = (*pBuf++) + 1;
/* ref: https://tools.ietf.org/html/rfc7540#section-6.1
The total number of padding octets is determined by the value of the Pad Length field. If the length of the padding is the length of the
frame payload or greater, the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->uPadding < pHeader->uLength)
{
pHeader->uLength -= pHeader->uPadding;
}
else
{
NetPrintf(("protohttp2: [%p] receive padding that is greater than the size of the frame, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return(NULL);
}
}
else
{
pHeader->uPadding = 0;
}
#if DIRTYCODE_LOGGING
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] received frame (len=%u, type=%s(%u), flags=%u, stream=0x%08x, padding=%u, skip=%s)\n",
pState, pHeader->uLength, PROTOHTTP2_GetFrameTypeStr(pHeader->uType), pHeader->uType, pHeader->uFlags, pHeader->iStreamId, pHeader->uPadding,
pHeader->bSkipFrame ? "TRUE" : "FALSE"));
#endif
return(pBuf);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeFrameHeader
\Description
Encodes the header of a http2 frame
\Input *pState - module state
\Input *pBuf - the buffer we are writing the data to
\Input *pHeader - the header data we are writing
\Version 09/29/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2EncodeFrameHeader(ProtoHttp2RefT *pState, uint8_t *pBuf, const FrameHeaderT *pHeader)
{
// encode 24-bit length
*pBuf++ = (uint8_t)(pHeader->uLength >> 16);
*pBuf++ = (uint8_t)(pHeader->uLength >> 8);
*pBuf++ = (uint8_t)(pHeader->uLength);
// encode 8-bit type
*pBuf++ = pHeader->uType;
// encode 8-bit flags
*pBuf++ = pHeader->uFlags;
// encode 31-bit stream id
*pBuf++ = (uint8_t)(pHeader->iStreamId >> 24);
*pBuf++ = (uint8_t)(pHeader->iStreamId >> 16);
*pBuf++ = (uint8_t)(pHeader->iStreamId >> 8);
*pBuf++ = (uint8_t)(pHeader->iStreamId);
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] encoding frame (len=%u, type=%s(%u), flags=%u, stream=0x%08x)\n",
pState, pHeader->uLength, _ProtoHttp2_strFrameType[pHeader->uType], pHeader->uType, pHeader->uFlags, pHeader->iStreamId));
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2DecodeSettings
\Description
Decodes the settings frame and saves the settings
\Input *pState - module state
\Input *pBuf - the buffer we are parsing the data from
\Input *pHeader - the header data for the frame
\Output
uint8_t - result of validation of the settings we received (TRUE=success, FALSE=failure)
\Version 09/29/2016 (eesponda)
*/
/********************************************************************************F*/
static uint8_t _ProtoHttp2DecodeSettings(ProtoHttp2RefT *pState, const uint8_t *pBuf, const FrameHeaderT *pHeader)
{
// if we received an ACK, commit our temp settings
if ((pHeader->uFlags & PROTOHTTP2_FLAG_ACK) != 0)
{
ds_memcpy(&pState->LocalSettings, &pState->TempSettings, sizeof(pState->LocalSettings));
}
else
{
int32_t iOffset;
for (iOffset = 0; iOffset < (int32_t)pHeader->uLength; iOffset += PROTOHTTP2_SETTING_SIZE)
{
uint16_t uKey;
uint32_t uValue;
// decode settings key
uKey = *pBuf++ << 8;
uKey |= *pBuf++;
// decode settings value
uValue = *pBuf++ << 24;
uValue |= *pBuf++ << 16;
uValue |= *pBuf++ << 8;
uValue |= *pBuf++;
switch (uKey)
{
case SETTINGS_ENABLE_PUSH:
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.5.2
Any value other than 0 or 1 MUST be treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if ((uValue != 0) && (uValue != 1))
{
NetPrintf(("protohttp2: [%p] received SETTINGS_ENABLE_PUSH with a value other than 0 or 1, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return(FALSE);
}
pState->PeerSettings.uEnablePush = uValue;
break;
}
case SETTINGS_HEADER_TABLE_SIZE: pState->PeerSettings.uHeaderTableSize = uValue; break;
case SETTINGS_INITIAL_WINDOW_SIZE:
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.5.2
Values above the maximum flow-control window size of 2^31-1 MUST be treated as a connection error (Section 5.4.1) of type
FLOW_CONTROL_ERROR. */
if (uValue > PROTOHTTP2_WINDOWSIZE_MAX)
{
NetPrintf(("protohttp2: [%p] received SETTINGS_INITIAL_WINDOW_SIZE with a value above our maximum window, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_FLOW_CONTROL_ERROR;
return(FALSE);
}
pState->PeerSettings.uInitialWindowSize = uValue;
break;
}
case SETTINGS_MAX_CONCURRENT_STREAM: pState->PeerSettings.uMaxConcurrentStream = uValue; break;
case SETTINGS_MAX_FRAME_SIZE:
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.5.2
The value advertised by an endpoint MUST be between this initial value and the maximum allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
Values outside this range MUST be treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if ((uValue < PROTOHTTP2_FRAMESIZE_MIN) || (uValue > PROTOHTTP2_FRAMESIZE_MAX))
{
NetPrintf(("protohttp2: [%p] received SETTINGS_MAX_FRAME_SIZE with a value outside our valid frame size range, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return(FALSE);
}
pState->PeerSettings.uMaxFrameSize = uValue;
break;
}
case SETTINGS_MAX_HEADER_LIST_SIZE: pState->PeerSettings.uMaxHeaderListSize = uValue; break;
default: NetPrintf(("protohttp2: [%p] unhandled settings key %u\n", pState, uKey)); break;
}
#if DIRTYCODE_LOGGING
if ((uKey >= SETTINGS_HEADER_TABLE_SIZE) && (uKey <= SETTINGS_MAX_HEADER_LIST_SIZE))
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] received setting (%s=%u)\n",
pState, _ProtoHttp2_strSettingName[uKey-1], uValue));
}
#endif
}
}
// success
return(TRUE);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2WriteSetting
\Description
Writes a setting into the payload
\Input *pState - module state
\Input *pBuf - [out] the buffer we are writing the data to
\Input *pHeader - the frame header we are updating while writing
\Input uSettingKey - the key for the setting
\Input uSettingValue - the value for the setting
\Output
int32_t - amount of data written
\Version 10/24/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2WriteSetting(ProtoHttp2RefT *pState, uint8_t *pBuf, FrameHeaderT *pHeader, uint16_t uSettingKey, uint32_t uSettingValue)
{
pHeader->uLength += PROTOHTTP2_SETTING_SIZE;
*pBuf++ = (uint8_t)(uSettingKey >> 8);
*pBuf++ = (uint8_t)(uSettingKey);
*pBuf++ = (uint8_t)(uSettingValue >> 24);
*pBuf++ = (uint8_t)(uSettingValue >> 16);
*pBuf++ = (uint8_t)(uSettingValue >> 8);
*pBuf++ = (uint8_t)(uSettingValue);
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] encode setting (%s=%u)\n",
pState, _ProtoHttp2_strSettingName[uSettingKey-1], uSettingValue));
return(PROTOHTTP2_SETTING_SIZE);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeSettings
\Description
Encodes the a settings frame
\Input *pState - module state
\Input bAck - are we acknowledging or updating settings?
\Input *pBuf - [out] the buffer we are writing the data to
\Input iBufLen - the length of the buffer
\Output
int32_t - amount of data written
\Version 09/29/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeSettings(ProtoHttp2RefT *pState, uint8_t bAck, uint8_t *pBuf, int32_t iBufLen)
{
FrameHeaderT Header;
uint8_t *pFrameHeader;
// make sure we have enough room for the maximum amount of settings
if (iBufLen < (PROTOHTTP2_HEADER_SIZE + (PROTOHTTP2_SETTING_SIZE * SETTINGS_NUM)))
{
return(0);
}
// setup defaults
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_SETTINGS;
// save the header location and advance the buffer
pFrameHeader = pBuf;
pBuf += PROTOHTTP2_HEADER_SIZE;
// if we need to acknowledge the peer's setting and our settings have not changed set the appropriate flags
if (bAck == TRUE)
{
Header.uFlags |= PROTOHTTP2_FLAG_ACK;
}
// otherwise, send our settings to the peer
else
{
if (pState->TempSettings.uHeaderTableSize != pState->LocalSettings.uHeaderTableSize)
{
pBuf += _ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_HEADER_TABLE_SIZE, pState->TempSettings.uHeaderTableSize);
}
if (pState->TempSettings.uEnablePush != pState->LocalSettings.uEnablePush)
{
pBuf += _ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_ENABLE_PUSH, pState->TempSettings.uEnablePush);
}
if (pState->TempSettings.uMaxConcurrentStream != pState->LocalSettings.uMaxConcurrentStream)
{
pBuf += _ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_MAX_CONCURRENT_STREAM, pState->TempSettings.uMaxConcurrentStream);
}
if (pState->TempSettings.uInitialWindowSize != pState->LocalSettings.uInitialWindowSize)
{
pBuf += _ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_INITIAL_WINDOW_SIZE, pState->TempSettings.uInitialWindowSize);
}
if (pState->TempSettings.uMaxFrameSize != pState->LocalSettings.uMaxFrameSize)
{
pBuf += _ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_MAX_FRAME_SIZE, pState->TempSettings.uMaxFrameSize);
}
if (pState->TempSettings.uMaxHeaderListSize != pState->LocalSettings.uMaxHeaderListSize)
{
_ProtoHttp2WriteSetting(pState, pBuf, &Header, SETTINGS_MAX_HEADER_LIST_SIZE, pState->TempSettings.uMaxHeaderListSize);
}
// ensure we actually wrote any settings
if (Header.uLength == 0)
{
return(0);
}
}
// encode the frame header, by this point we know we have enough space
_ProtoHttp2EncodeFrameHeader(pState, pFrameHeader, &Header);
return(Header.uLength+PROTOHTTP2_HEADER_SIZE);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeHeaders
\Description
Encodes the a headers frame
\Input *pState - module state
\Input *pStreamInfo - information about the stream (id, etc)
\Input bEndStream - does this request contain data?
\Input *pHeader - the header we are using to encode
\Output
int32_t - zero=success, negative=failure
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeHeaders(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, uint8_t bEndStream, const char *pHeader)
{
FrameHeaderT Header;
uint8_t *pFrameHeader, *pBuf;
int32_t iBufMax, iBufLen, iResult;
pBuf = pState->pOutBuf+pState->iOutLen;
iBufLen = PROTOHTTP2_CalculateFreeSpace(pState);
iBufMax = iBufLen-PROTOHTTP2_HEADER_SIZE;
// make sure we at least have enough space for frame header
if (iBufMax <= 0)
{
return(PROTOHTTP2_MINBUFF);
}
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_HEADERS;
Header.iStreamId = pStreamInfo->iStreamId;
Header.uFlags |= PROTOHTTP2_FLAG_END_HEADERS;
if (bEndStream == TRUE)
{
Header.uFlags |= PROTOHTTP2_FLAG_END_STREAM;
}
// save the header location and advance the buffer
pFrameHeader = pBuf;
pBuf += PROTOHTTP2_HEADER_SIZE;
// encode the header info the buffer
iResult = HpackEncode(pState->pEncoder, pHeader, pBuf, iBufMax, pState->bHuffman);
if ((iResult > 0) && (iResult < iBufMax))
{
Header.uLength = (uint32_t)iResult;
// encode the frame header, by this point we know we have enough space
_ProtoHttp2EncodeFrameHeader(pState, pFrameHeader, &Header);
// update the stream state
if (pStreamInfo->eState == STREAMSTATE_IDLE)
{
pStreamInfo->eState = (bEndStream == TRUE) ? STREAMSTATE_HALF_CLOSED_LOCAL : STREAMSTATE_OPEN;
}
// advance the buffer
pState->iOutLen += Header.uLength+PROTOHTTP2_HEADER_SIZE;
return(0);
}
return(-1);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeData
\Description
Encodes the a data frame
\Input *pState - module state
\Input *pStreamInfo - information about the stream (id, etc)
\Input *pInput - the payload data we are sending
\Input iInpLen - the length of the payload data
\Input bEndStream - will we be sending any more data frames?
\Output
int32_t - amount of data written
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeData(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, const uint8_t *pInput, int32_t iInpLen, uint8_t bEndStream)
{
FrameHeaderT Header;
uint8_t *pOutput = pState->pOutBuf+pState->iOutLen;
// if we are ending the stream we pull we include our reserved space
int32_t iOutLen = !bEndStream ? PROTOHTTP2_CalculateFreeSpace(pState) : pState->iOutMax-pState->iOutLen;
// make sure we have enough space to encode the frame header plus some more
if (iOutLen <= PROTOHTTP2_HEADER_SIZE)
{
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] not enough room in the output buffer to encode data (free-space=%d)\n",
pState, iOutLen));
return(PROTOHTTP2_MINBUFF);
}
iOutLen -= PROTOHTTP2_HEADER_SIZE;
/* if we cannot fit the whole user payload, then fill the remainder of the buffer
don't set the end stream flag as more data needs to be sent */
if (iInpLen > iOutLen)
{
iInpLen = iOutLen;
bEndStream = FALSE;
}
// make sure we have enough space in the window
if ((pState->iPeerWindow-iInpLen < 0) || (pStreamInfo->iPeerWindow-iInpLen < 0))
{
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] not encoding data as the server receive window is out of space (stream=0x%08x, conn window=%d, stream window=%d)\n",
pState, pStreamInfo->iStreamId, pState->iPeerWindow, pStreamInfo->iPeerWindow));
return(0);
}
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_DATA;
Header.iStreamId = pStreamInfo->iStreamId;
Header.uLength = iInpLen;
// end the stream if necessary
if (bEndStream == TRUE)
{
Header.uFlags |= PROTOHTTP2_FLAG_END_STREAM;
}
// encode the frame header
_ProtoHttp2EncodeFrameHeader(pState, pOutput, &Header);
pOutput += PROTOHTTP2_HEADER_SIZE;
// encode the data
ds_memcpy_s(pOutput, iOutLen, pInput, iInpLen);
// update the stream state
if (bEndStream == TRUE)
{
if (pStreamInfo->eState == STREAMSTATE_OPEN)
{
pStreamInfo->eState = STREAMSTATE_HALF_CLOSED_LOCAL;
}
else if (pStreamInfo->eState == STREAMSTATE_HALF_CLOSED_REMOTE)
{
pStreamInfo->eState = STREAMSTATE_CLOSED;
}
}
// advance the windows
pState->iPeerWindow -= iInpLen;
pStreamInfo->iPeerWindow -= iInpLen;
// advance buffer
pState->iOutLen += iInpLen+PROTOHTTP2_HEADER_SIZE;
// return amount of user payload data written to the output buffer
return(iInpLen);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeWindowUpdate
\Description
Encodes the a window update frame
\Input *pState - module state
\Input iStreamId - stream we are sending the window update for
\Input iIncrement - window increment we are sending for the stream
\Input *pWindow - [out] window we are incrementing
\Output
int32_t - zero=success, negative=error
\Notes
StreamId of 0 is reserved for updating the window of the entire
connection
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeWindowUpdate(ProtoHttp2RefT *pState, int32_t iStreamId, int32_t iIncrement, int32_t *pWindow)
{
FrameHeaderT Header;
const int32_t iPayloadSize = PROTOHTTP2_WINDOW_UPDATE_SIZE+PROTOHTTP2_HEADER_SIZE;
uint8_t *pBuf = pState->pOutBuf+pState->iOutLen;
int32_t iBufLen = pState->iOutMax-pState->iOutLen;
// make sure we have enough space to encode
if (iBufLen < iPayloadSize)
{
return(PROTOHTTP2_MINBUFF);
}
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_WINDOW_UPDATE;
Header.iStreamId = iStreamId;
Header.uLength = PROTOHTTP2_WINDOW_UPDATE_SIZE;
// encode the frame header
_ProtoHttp2EncodeFrameHeader(pState, pBuf, &Header);
pBuf += PROTOHTTP2_HEADER_SIZE;
// encode the data
*pBuf++ = (uint8_t)(iIncrement >> 24);
*pBuf++ = (uint8_t)(iIncrement >> 16);
*pBuf++ = (uint8_t)(iIncrement >> 8);
*pBuf++ = (uint8_t)(iIncrement);
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] incrementing the window (stream=0x%08x, size=%d, increment=%d)\n", pState, iStreamId, *pWindow, iIncrement));
*pWindow += iIncrement;
// advance the buffer
pState->iOutLen += iPayloadSize;
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodePing
\Description
Encodes a ping frame
\Input *pState - module state
\Input bAck - are we acknowledging a ping?
\Input *pInput - the opaque data we send
\Output
int32_t - zero=success, negative=error
\Version 10/26/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodePing(ProtoHttp2RefT *pState, uint8_t bAck, const uint8_t *pInput)
{
FrameHeaderT Header;
const int32_t iPayloadSize = PROTOHTTP2_PING_SIZE+PROTOHTTP2_HEADER_SIZE;
uint8_t *pOutput = pState->pOutBuf+pState->iOutLen;
int32_t iOutLen = pState->iOutMax-pState->iOutLen;
// make sure we have enough space to encode
if (iOutLen < iPayloadSize)
{
return(PROTOHTTP2_MINBUFF);
}
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_PING;
Header.uLength = PROTOHTTP2_PING_SIZE;
if (bAck == TRUE)
{
Header.uFlags |= PROTOHTTP2_FLAG_ACK;
}
// encode the frame header
_ProtoHttp2EncodeFrameHeader(pState, pOutput, &Header);
pOutput += PROTOHTTP2_HEADER_SIZE;
// encode the opaque data
ds_memcpy(pOutput, pInput, PROTOHTTP2_PING_SIZE);
// advance the buffer
pState->iOutLen += iPayloadSize;
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeGoAway
\Description
Encodes a goaway frame
\Input *pState - module state
\Input uErrorCode - error code being sent with frame
\Notes
This function cannot fail since we know that a goaway frame will always
fit within our output buffer. Since we are closing the connection right
after we can disregard any data already encoded there within.
\Version 10/31/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2EncodeGoAway(ProtoHttp2RefT *pState, uint32_t uErrorCode)
{
FrameHeaderT Header;
uint8_t *pBuf = pState->pOutBuf;
const int32_t iPayloadSize = PROTOHTTP2_GOAWAY_SIZE+PROTOHTTP2_HEADER_SIZE;
// the variable points to the next stream id so deduct 2 to get last assigned stream id
const int32_t iLastStreamId = pState->iStreamId - 2;
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_GOAWAY;
Header.uLength = PROTOHTTP2_GOAWAY_SIZE;
// encode the frame header
_ProtoHttp2EncodeFrameHeader(pState, pBuf, &Header);
pBuf += PROTOHTTP2_HEADER_SIZE;
// encode last-stream-id
*pBuf++ = (uint8_t)(iLastStreamId >> 24);
*pBuf++ = (uint8_t)(iLastStreamId >> 16);
*pBuf++ = (uint8_t)(iLastStreamId >> 8);
*pBuf++ = (uint8_t)(iLastStreamId);
// encode error code
*pBuf++ = (uint8_t)(uErrorCode >> 24);
*pBuf++ = (uint8_t)(uErrorCode >> 16);
*pBuf++ = (uint8_t)(uErrorCode >> 8);
*pBuf++ = (uint8_t)(uErrorCode);
pState->iOutLen = iPayloadSize;
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeRstStream
\Description
Encodes a rst_stream frame
\Input *pState - module state
\Input iStreamId - the stream we are resetting
\Input uErrorCode - the error that caused the reset
\Output
int32_t - zero=success, negative=failure
\Version 11/03/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeRstStream(ProtoHttp2RefT *pState, int32_t iStreamId, uint32_t uErrorCode)
{
FrameHeaderT Header;
const int32_t iPayloadSize = PROTOHTTP2_RST_STREAM_SIZE+PROTOHTTP2_HEADER_SIZE;
uint8_t *pBuf = pState->pOutBuf+pState->iOutLen;
int32_t iBufSize = pState->iOutMax-pState->iOutLen;
// make sure we have enough space to encode
if (iBufSize < iPayloadSize)
{
return(PROTOHTTP2_MINBUFF);
}
// setup the frame header
ds_memclr(&Header, sizeof(Header));
Header.uType = FRAMETYPE_RST_STREAM;
Header.uLength = PROTOHTTP2_RST_STREAM_SIZE;
Header.iStreamId = iStreamId;
// encode the frame header
_ProtoHttp2EncodeFrameHeader(pState, pBuf, &Header);
pBuf += PROTOHTTP2_HEADER_SIZE;
// encode the error code
*pBuf++ = (uint8_t)(uErrorCode >> 24);
*pBuf++ = (uint8_t)(uErrorCode >> 16);
*pBuf++ = (uint8_t)(uErrorCode >> 8);
*pBuf++ = (uint8_t)(uErrorCode);
// advance the buffer
pState->iOutLen += iPayloadSize;
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2SendGoAway
\Description
Sends goaway frame to peer to signal closing the connection
\Input *pState - module state
\Input eErrorType - the error we are sending in the goaway
\Input *pReason - logging reason we are closing the connection
\Version 10/31/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2SendGoAway(ProtoHttp2RefT *pState, ErrorTypeE eErrorType, const char *pReason)
{
// only send this error if still connected
if (ProtoSSLStat(pState->pSsl, 'stat', NULL, 0) >= 0)
{
int32_t iStream;
// encode and send go away
_ProtoHttp2EncodeGoAway(pState, eErrorType);
_ProtoHttp2Send(pState, pState->pOutBuf, pState->iOutLen);
// close all the streams that are currently active
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamCloseOnError(pState, &pState->Streams[iStream]);
}
// close the connection
_ProtoHttp2Close(pState, pReason);
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2PrepareRstStream
\Description
Prepares the rst_stream to be sent on the next update
\Input *pState - module state
\Input *pStreamInfo - information about stream we are resetting
\Input eErrorType - reason for closing the connection
\Input *pMessage - logging message about why we are resetting
\Version 11/21/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2PrepareRstStream(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, ErrorTypeE eErrorType, const char *pMessage)
{
NetPrintf(("protohttp2: [%p] %s, sending %s to peer\n", pState, pMessage, _ProtoHttp2_strErrorType[eErrorType]));
if (_ProtoHttp2EncodeRstStream(pState, pStreamInfo->iStreamId, eErrorType) != 0)
{
NetPrintf(("protohttp2: [%p] not enough space in output buffer to encode rst_stream\n", pState));
}
_ProtoHttp2StreamCloseOnError(pState, pStreamInfo);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2ParseHeader
\Description
Decodes and parses the header frame information
\Input *pState - module state
\Input *pStreamInfo - stream information
\Input *pBuf - the buffer we are parsing the data from
\Input iBufLen - length of the buffer
\Output
int32_t - success=zero, failure=negative
\Version 10/27/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2ParseHeader(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, const uint8_t *pBuf, int32_t iBufLen)
{
char strValue[128], *pHeader;
int32_t iResult, iHeaderMax;
ProtoHttp2ReceiveHeaderCbT *pReceiveHeaderCb;
void *pUserData;
const uint8_t bParse = pStreamInfo->iHeaderLen == 0; /* if this is the first header, we need to parse */
// allocate memory for uncompressed headers
if ((pStreamInfo->pHeader == NULL) && ((pStreamInfo->pHeader = (char *)DirtyMemAlloc(pState->LocalSettings.uMaxHeaderListSize, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData)) == NULL))
{
NetPrintf(("protohttp2: [%p] could not allocate space for header list\n", pState));
return(-1);
}
// point to the memory we are updating
pHeader = pStreamInfo->pHeader+pStreamInfo->iHeaderLen;
iHeaderMax = pState->LocalSettings.uMaxHeaderListSize-pStreamInfo->iHeaderLen;
// decode the header and write the information to our cache
if ((iResult = HpackDecode(pState->pDecoder, pBuf, iBufLen, pHeader, iHeaderMax)) >= 0)
{
pStreamInfo->iHeaderLen += iResult;
}
else
{
NetPrintf(("protohttp2: [%p] could not decode the incoming headers\n", pState));
return(-1);
}
/* if we already received an initial header, we don't need to reparse this data as
we are just handling trailing header fields after the last data chunk.
these fields _should_ not contain any of the headers we are parsing for */
if (bParse == TRUE)
{
// parse header code
pStreamInfo->eResponseCode = ProtoHttpParseHeaderCode(pStreamInfo->pHeader);
// parse content-length field
if (ProtoHttpGetHeaderValue(pState, pStreamInfo->pHeader, "content-length", strValue, sizeof(strValue), NULL) != -1)
{
pStreamInfo->iBodySize = ds_strtoll(strValue, NULL, 10);
}
else
{
pStreamInfo->iBodySize = -1;
}
// parse last-modified header
if (ProtoHttpGetHeaderValue(pState, pStreamInfo->pHeader, "last-modified", strValue, sizeof(strValue), NULL) != -1)
{
pStreamInfo->iHdrDate = (int32_t)ds_strtotime(strValue);
}
#if DIRTYCODE_LOGGING
// if this is a redirect, we don't support it log something
if (PROTOHTTP_GetResponseClass(pStreamInfo->eResponseCode) == PROTOHTTP_RESPONSE_REDIRECTION)
{
NetPrintf(("protohttp2: [%p] auto-redirection not supported, if you want to follow the redirect please issue a new request\n", pState));
}
#endif
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] received %d response (%d bytes)\n", pState, pStreamInfo->eResponseCode, pStreamInfo->iHeaderLen));
}
#if DIRTYCODE_LOGGING
if (pState->iVerbose > 1)
{
NetPrintf(("protohttp2: [%p] received response header:\n", pState));
NetPrintWrap(pHeader, 80);
}
#endif
// request level callback takes priority to global
if ((pReceiveHeaderCb = pStreamInfo->pReceiveHeaderCb) != NULL)
{
pUserData = pStreamInfo->pUserData;
}
else
{
pReceiveHeaderCb = pState->pReceiveHeaderCb;
pUserData = pState->pCallbackRef;
}
// if we have a receive header callback, invoke it
if (pReceiveHeaderCb != NULL)
{
pReceiveHeaderCb(pState, pStreamInfo->iStreamId, pHeader, iResult, pUserData);
}
// header successfully parsed
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2HandleData
\Description
Handles the incoming data frame
\Input *pState - module state
\Input *pBuf - the buffer we are parsing the data from
\Input *pHeader - the header data for the frame
\Input *pStreamInfo - information about associated stream
\Version 11/04/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2HandleData(ProtoHttp2RefT *pState, const uint8_t *pBuf, const FrameHeaderT *pHeader, StreamInfoT *pStreamInfo)
{
uint8_t bEndStream = FALSE;
// check the state of the stream to make sure we are in correct state
if ((pStreamInfo->eState != STREAMSTATE_OPEN) && (pStreamInfo->eState != STREAMSTATE_HALF_CLOSED_LOCAL))
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_STREAM_CLOSED, "received data frame when in unexpected state");
return;
}
// update the stream data based on the flags
if ((pHeader->uFlags & PROTOHTTP2_FLAG_END_STREAM) != 0)
{
if (pStreamInfo->eState == STREAMSTATE_OPEN)
{
pStreamInfo->eState = STREAMSTATE_HALF_CLOSED_REMOTE;
}
else if (pStreamInfo->eState == STREAMSTATE_HALF_CLOSED_LOCAL)
{
pStreamInfo->eState = STREAMSTATE_CLOSED;
}
bEndStream = TRUE;
}
// if we have a write callback, invoke it
if (pStreamInfo->pWriteCb != NULL)
{
ProtoHttp2WriteCbInfoT CbInfo;
CbInfo.iStreamId = pStreamInfo->iStreamId;
CbInfo.eRequestType = pStreamInfo->eRequestType;
CbInfo.eRequestResponse = pStreamInfo->eResponseCode;
pStreamInfo->pWriteCb(pState, &CbInfo, pBuf, pHeader->uLength, pStreamInfo->pUserData);
pState->iLocalWindow -= pHeader->uLength;
pStreamInfo->iLocalWindow -= pHeader->uLength;
// update amount of data received
pStreamInfo->iBodyReceived += pHeader->uLength;
if (bEndStream == TRUE)
{
// update body size now that stream is complete
pStreamInfo->iBodySize = pStreamInfo->iBodyReceived;
pStreamInfo->pWriteCb(pState, &CbInfo, NULL, PROTOHTTP2_RECVDONE, pStreamInfo->pUserData);
}
}
else
{
// allocate space if necessary (size of the window)
if ((pStreamInfo->pData == NULL) && ((pStreamInfo->pData = (uint8_t *)DirtyMemAlloc(pStreamInfo->iDataMax, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData)) == NULL))
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_INTERNAL_ERROR, "failed to allocate space for storing data");
return;
}
// make sure the peer did not exceed the window
if (pStreamInfo->iDataLen+(int32_t)pHeader->uLength <= pStreamInfo->iDataMax)
{
ds_memcpy(pStreamInfo->pData+pStreamInfo->iDataLen, pBuf, pHeader->uLength);
pStreamInfo->iDataLen += pHeader->uLength;
pState->iLocalWindow -= pHeader->uLength;
pStreamInfo->iLocalWindow -= pHeader->uLength;
}
else
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_FLOW_CONTROL_ERROR, "peer exceeded the receive window");
}
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2HandleHeaders
\Description
Handles the incoming headers frame
\Input *pState - module state
\Input *pBuf - the buffer we are parsing the data from
\Input *pHeader - the header data for the frame
\Input *pStreamInfo - information about associated stream
\Version 11/04/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2HandleHeaders(ProtoHttp2RefT *pState, const uint8_t *pBuf, const FrameHeaderT *pHeader, StreamInfoT *pStreamInfo)
{
uint8_t bEndStream = FALSE;
int32_t iBufRead = 0;
if ((pStreamInfo->eState != STREAMSTATE_OPEN) && (pStreamInfo->eState != STREAMSTATE_HALF_CLOSED_LOCAL))
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_STREAM_CLOSED, "received header frames when stream is in unexpected state");
return;
}
// update the stream data based on the flags
if ((pHeader->uFlags & PROTOHTTP2_FLAG_END_STREAM) != 0)
{
if (pStreamInfo->eState == STREAMSTATE_OPEN)
{
pStreamInfo->eState = STREAMSTATE_HALF_CLOSED_REMOTE;
}
else if (pStreamInfo->eState == STREAMSTATE_HALF_CLOSED_LOCAL)
{
pStreamInfo->eState = STREAMSTATE_CLOSED;
}
bEndStream = TRUE;
}
// skip the priority frame data if present
if ((pHeader->uFlags & PROTOHTTP2_FLAG_PRIORITY) != 0)
{
iBufRead += PROTOHTTP2_PRIORITY_SIZE;
}
// if this is the end of the stream we can parse our headers
if ((pHeader->uFlags & PROTOHTTP2_FLAG_END_HEADERS) != 0)
{
if (_ProtoHttp2ParseHeader(pState, pStreamInfo, pBuf+iBufRead, pHeader->uLength-iBufRead) != 0)
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_COMPRESSION_ERROR, "failed to decompress header");
}
}
else
{
NetPrintf(("protohttp2: [%p] expected end of the header as our maximum header list fits within a single frame, closing connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
// if we have a write callback, invoke it
if ((pStreamInfo->pWriteCb != NULL) && (bEndStream == TRUE))
{
ProtoHttp2WriteCbInfoT CbInfo;
CbInfo.iStreamId = pStreamInfo->iStreamId;
CbInfo.eRequestType = pStreamInfo->eRequestType;
CbInfo.eRequestResponse = pStreamInfo->eResponseCode;
// update body size now that stream is complete
pStreamInfo->iBodySize = pStreamInfo->iBodyReceived;
pStreamInfo->pWriteCb(pState, &CbInfo, NULL, PROTOHTTP2_RECVDONE, pStreamInfo->pUserData);
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2HandleFrame
\Description
Handles the incoming http2 frame
\Input *pState - module state
\Input *pBuf - the buffer we are parsing the data from
\Input *pHeader - the header data for the frame
\Input *pStreamInfo - stream information
\Version 09/29/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2HandleFrame(ProtoHttp2RefT *pState, const uint8_t *pBuf, const FrameHeaderT *pHeader, StreamInfoT *pStreamInfo)
{
int32_t iOffset;
// don't do any processing if we were told to skip
if (pHeader->bSkipFrame == TRUE)
{
return;
}
if (pHeader->uType == FRAMETYPE_SETTINGS)
{
const int32_t iPrevWindow = pState->PeerSettings.uInitialWindowSize; /* save previous window setting for calculation */
/* ref: https://tools.ietf.org/html/rfc7540#section-6.5
SETTINGS frames always apply to a connection, never a single stream. The stream identifier for a SETTINGS frame MUST be zero (0x0).
If an endpoint receives a SETTINGS frame whose stream identifier field is anything other than 0x0, the endpoint MUST respond with
a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId != 0)
{
NetPrintf(("protohttp2: [%p] received settings frame with stream id not 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return;
}
/* ref: https://tools.ietf.org/html/rfc7540#section-6.5
A SETTINGS frame with a length other than a multiple of 6 octets MUST be treated as a connection error (Section 5.4.1) of type
FRAME_SIZE_ERROR. */
else if ((pHeader->uLength % PROTOHTTP2_SETTING_SIZE) != 0)
{
NetPrintf(("protohttp2: [%p] received settings frame with invalid size %d, closing the connection\n", pState, pHeader->uLength));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
return;
}
// decode the settings and update what we have saved
if (_ProtoHttp2DecodeSettings(pState, pBuf, pHeader) == FALSE)
{
return;
}
// if not an acknowledgement, apply settings and send acknowledgement
if ((pHeader->uFlags & PROTOHTTP2_FLAG_ACK) == 0)
{
int32_t iStream;
// encode the acknowledge
if ((iOffset = _ProtoHttp2EncodeSettings(pState, TRUE, pState->pOutBuf+pState->iOutLen, pState->iOutMax-pState->iOutLen)) != 0)
{
pState->iOutLen += iOffset;
}
// resize the encoder dynamic table to match the peer, send dynamic table update if needed
HpackResize(pState->pEncoder, pState->PeerSettings.uHeaderTableSize, pState->bSettingsRecv);
pState->bSettingsRecv = TRUE;
// recalculate the stream windows based on new settings and how much we consumed
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
StreamInfoT *pStream = &pState->Streams[iStream];
if ((pStream->eState == STREAMSTATE_OPEN) || (pStream->eState == STREAMSTATE_HALF_CLOSED_LOCAL))
{
pStream->iPeerWindow = pState->PeerSettings.uInitialWindowSize - (iPrevWindow - pStream->iPeerWindow);
}
}
}
else
{
// received an acknowledgement, disable timer
pState->uSettingsTimer = 0;
}
}
else if (pHeader->uType == FRAMETYPE_RST_STREAM)
{
uint32_t uErrorCode;
/* ref: https://tools.ietf.org/html/rfc7540#section-6.4
RST_STREAM frames MUST be associated with a stream. If a RST_STREAM frame is received with a stream identifier of 0x0, the recipient MUST
treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId == 0)
{
NetPrintf(("protohttp2: [%p] received rst_stream frame with stream id 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
return;
}
/* ref: https://tools.ietf.org/html/rfc7540#section-6.4
A RST_STREAM frame with a length other than 4 octets MUST be treated as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. */
else if (pHeader->uLength != PROTOHTTP2_RST_STREAM_SIZE)
{
NetPrintf(("protohttp2: [%p] received rst_stream frame with invalid size %d, closing the connection\n", pState, pHeader->uLength));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
return;
}
// decode error code
uErrorCode = *pBuf++ << 24;
uErrorCode |= *pBuf++ << 16;
uErrorCode |= *pBuf++ << 8;
uErrorCode |= *pBuf++;
NetPrintf(("protohttp2: [%p] received rst stream (error=%s)\n", pState, _ProtoHttp2_strErrorType[uErrorCode]));
// cache the error code
pStreamInfo->eErrorType = (ErrorTypeE)uErrorCode;
// close the stream and fire any callbacks needed
_ProtoHttp2StreamCloseOnError(pState, pStreamInfo);
}
else if (pHeader->uType == FRAMETYPE_GOAWAY)
{
int32_t iStreamId = 0, iStream;
uint32_t uErrorCode;
char strDebugData[256] = "";
/* ref: https://tools.ietf.org/html/rfc7540#section-6.8
The GOAWAY frame applies to the connection, not a specific stream. An endpoint MUST treat a GOAWAY frame with a stream identifier other
than 0x0 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId != 0)
{
NetPrintf(("protohttp2: [%p] received goaway frame with stream id not 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
/* ref: https://tools.ietf.org/html/rfc7540#section-6.8
the GOAWAY frame doesn't have any requirements about frame size but I need to ensure that we at least received the required amount of data */
else if (pHeader->uLength < PROTOHTTP2_GOAWAY_SIZE)
{
NetPrintf(("protohttp2: [%p] received goway frame with invalid size %d, closing the connection\n", pState, pHeader->uLength));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
}
// if we can decode it since no error occured, do that and save the error type
if (pState->eErrorType == ERRORTYPE_NO_ERROR)
{
// decode last-stream-id
iStreamId = *pBuf++ << 24;
iStreamId |= *pBuf++ << 16;
iStreamId |= *pBuf++ << 8;
iStreamId |= *pBuf++;
// decode error code
uErrorCode = *pBuf++ << 24;
uErrorCode |= *pBuf++ << 16;
uErrorCode |= *pBuf++ << 8;
uErrorCode |= *pBuf++;
// decode debug data
ds_strsubzcpy(strDebugData, sizeof(strDebugData), (const char *)pBuf, pHeader->uLength-PROTOHTTP2_GOAWAY_SIZE);
// cache the error the peer sent
pState->eErrorType = (ErrorTypeE)uErrorCode;
}
NetPrintf(("protohttp2: [%p] received goaway frame (stream=0x%08x, error=%s, debug=%s)\n", pState, iStreamId, _ProtoHttp2_strErrorType[pState->eErrorType], strDebugData));
// close all the streams
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamCloseOnError(pState, &pState->Streams[iStream]);
}
// peer told us to go away, set the state and (attempt to) close the connection
pState->eState = ST_FAIL;
_ProtoHttp2Close(pState, "goaway");
}
else if (pHeader->uType == FRAMETYPE_PING)
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.7
PING frames are not associated with any individual stream. If a PING frame is received with a stream identifier field value other than
0x0, the recipient MUST respond with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId != 0)
{
NetPrintf(("protohttp2: [%p] received ping frame with stream id not 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
/* ref: https://tools.ietf.org/html/rfc7540#section-6.7
Receipt of a PING frame with a length field value other than 8 MUST be treated as a connection error (Section 5.4.1) of type
FRAME_SIZE_ERROR. */
else if (pHeader->uLength != PROTOHTTP2_PING_SIZE)
{
NetPrintf(("protohttp2: [%p] received ping frame with incorrect length %d, closing the connection\n", pState, pHeader->uLength));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
}
else if ((pHeader->uFlags & PROTOHTTP2_FLAG_ACK) == 0)
{
// encode ping acknowledgement
if ((iOffset = _ProtoHttp2EncodePing(pState, TRUE, pBuf)) != 0)
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] not enough space to encode ping frame into output buffer\n", pState));
}
}
else
{
// we received an acknowlegement, reset timer
pState->uPingTimer = 0;
}
}
else if (pHeader->uType == FRAMETYPE_HEADERS)
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.2
HEADERS frames MUST be associated with a stream. If a HEADERS frame is received whose stream identifier field is 0x0, the recipient MUST
respond with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId != 0)
{
_ProtoHttp2HandleHeaders(pState, pBuf, pHeader, pStreamInfo);
}
else
{
NetPrintf(("protohttp2: [%p] received headers frame with stream id 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
}
else if (pHeader->uType == FRAMETYPE_DATA)
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.1
DATA frames MUST be associated with a stream. If a DATA frame is received whose stream identifier field is 0x0, the recipient MUST
respond with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. */
if (pHeader->iStreamId != 0)
{
_ProtoHttp2HandleData(pState, pBuf, pHeader, pStreamInfo);
}
else
{
NetPrintf(("protohttp2: [%p] received data frame with stream id 0, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
}
else if (pHeader->uType == FRAMETYPE_WINDOW_UPDATE)
{
int32_t iIncrement;
/* ref: https://tools.ietf.org/html/rfc7540#section-6.9
A WINDOW_UPDATE frame with a length other than 4 octets MUST be treated as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR. */
if (pHeader->uLength != PROTOHTTP2_WINDOW_UPDATE_SIZE)
{
NetPrintf(("protohttp2: [%p] received window update frame with incorrect length %d, closing the connection\n", pState, pHeader->uLength));
pState->eErrorType = ERRORTYPE_FRAME_SIZE_ERROR;
return;
}
// decode 31-bit window size increment (ignoring the high bit)
iIncrement = (*pBuf++ & 0x7f) << 24;
iIncrement |= *pBuf++ << 16;
iIncrement |= *pBuf++ << 8;
iIncrement |= *pBuf++;
// make sure the window size increment is valid
if (iIncrement != 0)
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] received window update (stream=0x%08x, increment=%d)\n", pState, pHeader->iStreamId, iIncrement));
if (pHeader->iStreamId == 0)
{
pState->iPeerWindow += iIncrement;
}
else
{
pStreamInfo->iPeerWindow += iIncrement;
}
}
else
{
/* ref: https://tools.ietf.org/html/rfc7540#section-6.9
A receiver MUST treat the receipt of a WINDOW_UPDATE frame with an flow-control window increment of 0 as a stream error (Section 5.4.2)
of type PROTOCOL_ERROR; errors on the connection flow-control window MUST be treated as a connection error (Section 5.4.1). */
if (pHeader->iStreamId == 0)
{
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
NetPrintf(("protohttp2: [%p] received invalid connection wide window size increment, closing connection\n", pState));
}
else
{
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_PROTOCOL_ERROR, "received invalid stream window size increment");
}
}
}
else if (pHeader->uType == FRAMETYPE_CONTINUATION)
{
/* we should never receive continuation as our header list maximum
is well below what we can send in a frame */
NetPrintf(("protohttp2: [%p] received continuation frame when not expected, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
else if (pHeader->uType == FRAMETYPE_PUSH_PROMISE)
{
/* we disabled push in our settings so if we receive this type of frame
it is a protocol level error */
NetPrintf(("protohttp2: [%p] received push promise frame when push was disabled, closing the connection\n", pState));
pState->eErrorType = ERRORTYPE_PROTOCOL_ERROR;
}
/* ref: https://tools.ietf.org/html/rfc7540#section-4.1
Implementations MUST ignore and discard any frame that has a type that is unknown. */
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2FormatRequestHeader
\Description
Format a request header based on given input data.
\Input *pState - module state
\Input *pStreamInfo - stream information for the request
\Input *pUrl - pointer to user-supplied url
\Input iDataLen - size of included data; zero if none, negative if streaming put/post
\Input **pOutHdr - [out] output of the formatting
\Output
int32_t - zero=success, else error
\Version 10/24/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2FormatRequestHeader(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, const char *pUrl, int32_t iDataLen, char **pOutHdr)
{
const char *pUrlSlash;
char *pHeader;
int32_t iHeaderLen = 0, iHeaderMax;
ProtoHttp2CustomHeaderCbT *pCustomHeaderCb;
void *pUserData;
// if url is empty or isn't preceded by a slash, put one in
pUrlSlash = (*pUrl != '/') ? "/" : "";
// set up for header formatting
pHeader = (char *)pState->pOutBuf + pState->iOutLen;
iHeaderMax = pState->iOutMax - pState->iOutLen;
// format request header
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, ":method: %s\r\n", _ProtoHttp2_strRequestNames[pStreamInfo->eRequestType]);
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, ":path: %s%s\r\n", pUrlSlash, pUrl);
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, ":scheme: %s\r\n", pState->bSecure ? "https" : "http");
if ((pState->bSecure && (pState->iPort == 443)) || (pState->iPort == 80))
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, ":authority: %s\r\n", pState->strHost);
}
else
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, ":authority: %s:%d\r\n", pState->strHost, pState->iPort);
}
if (iDataLen > 0)
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, "content-length: %qd\r\n", iDataLen);
}
if ((pState->pAppendHdr == NULL) || !ds_stristr(pState->pAppendHdr, "user-agent:"))
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, "user-agent: ProtoHttp %d.%d/DS %d.%d.%d.%d.%d (" DIRTYCODE_PLATNAME ")\r\n",
(PROTOHTTP2_VERSION >> 8) & 0xFF, PROTOHTTP2_VERSION & 0xFF, DIRTYSDK_VERSION_YEAR, DIRTYSDK_VERSION_SEASON, DIRTYSDK_VERSION_MAJOR, DIRTYSDK_VERSION_MINOR, DIRTYSDK_VERSION_PATCH);
}
if ((pState->pAppendHdr == NULL) || (pState->pAppendHdr[0] == '\0'))
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, "accept: */*\r\n");
}
else
{
iHeaderLen += ds_snzprintf(pHeader+iHeaderLen, iHeaderMax-iHeaderLen, "%s", pState->pAppendHdr);
}
// null-terminate the buffer
pHeader[iHeaderLen++] = '\0';
// request level callback takes priority to global
if ((pCustomHeaderCb = pStreamInfo->pCustomHeaderCb) != NULL)
{
pUserData = pStreamInfo->pUserData;
}
else
{
pCustomHeaderCb = pState->pCustomHeaderCb;
pUserData = pState->pCallbackRef;
}
// call custom header format callback, if specified
if (pCustomHeaderCb != NULL)
{
if ((iHeaderLen = pCustomHeaderCb(pState, pHeader, iHeaderMax, NULL, 0, pUserData)) < 0)
{
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] custom header callback error %d\n", pState, iHeaderLen));
return(iHeaderLen);
}
if (iHeaderLen == 0)
{
iHeaderLen = (int32_t)strlen(pHeader);
}
}
// make sure we were able to complete the header
if (iHeaderLen > iHeaderMax)
{
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] not enough buffer to format request header (have %d, need %d)\n", pState, iHeaderMax, iHeaderLen));
return(PROTOHTTP2_MINBUFF);
}
// make sure that the size doesn't exceed what the server supports
else if (iHeaderLen > (signed)pState->PeerSettings.uMaxHeaderListSize)
{
NetPrintf(("protohttp2: [%p] the formatted (uncompressed) header exceeds the size that the server supports (have %u, need %d)\n",
pState, pState->PeerSettings.uMaxHeaderListSize, iHeaderLen));
return(-1);
}
// save a copy of the header
ds_strnzcpy(pStreamInfo->strRequestHeader, pHeader, sizeof(pStreamInfo->strRequestHeader));
#if DIRTYCODE_LOGGING
if (pState->iVerbose > 1)
{
NetPrintf(("protohttp2: [%p] sending request:\n", pState));
NetPrintWrap(pHeader, 80);
}
#endif
/* allocate a temporary buffer to store the uncompressed buffer. we cannot use the current spot we have written the
data to because the compress header needs to be written to the same place before being sent on the wire.
this will not exceed our frame size */
if ((*pOutHdr = (char *)DirtyMemAlloc(iHeaderLen+1, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData)) == NULL)
{
NetPrintf(("protohttp2: [%p] could not allocate space for request header compression\n", pState));
return(-1);
}
ds_strnzcpy(*pOutHdr, pHeader, iHeaderLen+1);
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2SetAppendHeader
\Description
Set given string as append header, allocating memory as required
\Input *pState - module state
\Input *pAppendHdr - append header string
\Output
int32_t - zero=success, else error
\Version 10/27/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2SetAppendHeader(ProtoHttp2RefT *pState, const char *pAppendHdr)
{
int32_t iAppendBufLen, iAppendStrLen;
// check for empty append string, in which case we free the buffer
if ((pAppendHdr == NULL) || (*pAppendHdr == '\0'))
{
if (pState->pAppendHdr != NULL)
{
DirtyMemFree(pState->pAppendHdr, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
pState->pAppendHdr = NULL;
}
pState->iAppendLen = 0;
return(0);
}
// check to see if append header is already set
if ((pState->pAppendHdr != NULL) && (strcmp(pAppendHdr, pState->pAppendHdr) == 0))
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] ignoring set of append header '%s' that is already set\n", pState, pAppendHdr));
return(0);
}
// get append header length
iAppendStrLen = (int32_t)strlen(pAppendHdr);
// append buffer size includes null and space for \r\n if not included by submitter
iAppendBufLen = iAppendStrLen + 3;
// see if we need to allocate a new buffer
if (iAppendBufLen > pState->iAppendLen)
{
if (pState->pAppendHdr != NULL)
{
DirtyMemFree(pState->pAppendHdr, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
}
if ((pState->pAppendHdr = DirtyMemAlloc(iAppendBufLen, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData)) != NULL)
{
pState->iAppendLen = iAppendBufLen;
}
else
{
NetPrintf(("protohttp2: [%p] could not allocate %d byte buffer for append header\n", pState, iAppendBufLen));
pState->iAppendLen = 0;
return(-1);
}
}
// copy append header
ds_strnzcpy(pState->pAppendHdr, pAppendHdr, iAppendStrLen+1);
// if append header is not \r\n terminated, do it here
if ((pAppendHdr[iAppendStrLen-2] != '\r') || (pAppendHdr[iAppendStrLen-1] != '\n'))
{
ds_strnzcat(pState->pAppendHdr, "\r\n", pState->iAppendLen);
}
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2ResizeInputBuffer
\Description
Resize the input buffer
\Input *pState - module state
\Input iBufMax - new buffer size
\Output
int32_t - zero=success, else error
\Version 11/14/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2ResizeInputBuffer(ProtoHttp2RefT *pState, int32_t iBufMax)
{
int32_t iCopySize;
uint8_t *pInpBuf;
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] resizing input buffer from %d to %d bytes\n", pState, pState->iInpMax, iBufMax));
if ((pInpBuf = (uint8_t *)DirtyMemAlloc(iBufMax, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData)) == NULL)
{
NetPrintf(("protohttp2: [%p] could not resize input buffer\n", pState));
return(-1);
}
// calculate size of data to copy from old buffer to new
if ((iCopySize = pState->iInpLen) > iBufMax)
{
NetPrintf(("protohttp2: [%p] warning; resize of input buffer is losing %d bytes of data\n", pState, iCopySize - iBufMax));
iCopySize = iBufMax;
}
// copy valid contents of input buffer, if any, to new buffer
ds_memcpy(pInpBuf, pState->pInpBuf, iCopySize);
// dispose of old buffer
DirtyMemFree(pState->pInpBuf, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
// update buffer variables
pState->pInpBuf = pInpBuf;
pState->iInpLen = iCopySize;
pState->iInpMax = iBufMax;
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2CheckSettings
\Description
Handle all setting related synchronization
\Input *pState - module state
\Notes
This function will check to make sure that we get our settings
acknowledgement from our peer in time. It also checks to see
if we need to send a new settings frame to our peer, in this
case it will handle the encoding into our output buffer.
\Version 11/15/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2CheckSettings(ProtoHttp2RefT *pState)
{
// check for settings timeout
if (pState->uSettingsTimer != 0)
{
if (NetTickDiff(NetTick(), pState->uSettingsTimer) > 0)
{
NetPrintf(("protohttp2: [%p] peer did not send settings in time, closing connection\n", pState));
pState->eErrorType = ERRORTYPE_SETTINGS_TIMEOUT;
}
}
// otherwise check if we need to synchronize our settings
else if (memcmp(&pState->LocalSettings, &pState->TempSettings, sizeof(pState->TempSettings)) != 0)
{
int32_t iOffset;
if ((iOffset = _ProtoHttp2EncodeSettings(pState, FALSE, pState->pOutBuf+pState->iOutLen, pState->iOutMax-pState->iOutLen)) != 0)
{
pState->iOutLen += iOffset;
pState->uSettingsTimer = NetTick() + PROTOHTTP2_TIMEOUT;
}
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2CheckWindows
\Description
Check the connection and stream wide windows to send increments when
necessary
\Input *pState - module state
\Version 11/21/2016 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2CheckWindows(ProtoHttp2RefT *pState)
{
int32_t iStream;
// check the connection wide window, if so increment a large amount to handle our multiple streams
if (pState->iLocalWindow <= (PROTOHTTP2_MAX_STREAMS*PROTOHTTP2_WINDOWSIZE))
{
if (_ProtoHttp2EncodeWindowUpdate(pState, 0, PROTOHTTP2_MAX_STREAMS*PROTOHTTP2_WINDOWSIZE, &pState->iLocalWindow) != 0)
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] not enough space in the output buffer to encode window update for connection\n", pState));
}
}
// check the stream wide windows
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
StreamInfoT *pStreamInfo = &pState->Streams[iStream];
if (pStreamInfo->iLocalWindow <= PROTOHTTP2_WINDOWSIZE)
{
if (_ProtoHttp2EncodeWindowUpdate(pState, pStreamInfo->iStreamId, PROTOHTTP2_WINDOWSIZE, &pStreamInfo->iLocalWindow) != 0)
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] not enough space in the output buffer to encode window update for stream\n", pState));
}
}
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2CheckActivityTimeout
\Description
If we lack i/o activity ping the server to make sure the connection is still
active. If the don't receive an ack in time we then close the connection.
\Input *pState - module state
\Version 08/13/2018 (eesponda)
*/
/********************************************************************************F*/
static void _ProtoHttp2CheckActivityTimeout(ProtoHttp2RefT *pState)
{
/* if the ping timer isn't active check if our connection has been idle, if so then lets ping the server to see if
we have a valid connection. */
if (pState->uPingTimer == 0)
{
if (NetTickDiff(NetTick(), pState->uTimer) > 0)
{
uint8_t aInput[PROTOHTTP2_PING_SIZE];
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] idle after %us, sending ping to check if connection is still active\n",
pState, pState->uTimeout/1000));
// encode ping to check if connection is active
if (_ProtoHttp2EncodePing(pState, FALSE, aInput) == 0)
{
pState->uPingTimer = NetTick() + PROTOHTTP2_TIMEOUT_DEFAULT;
}
else
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] not enough space to encode ping frame into output buffer\n", pState));
}
}
}
// otherwise if the ping timer is active, close the connection if the server doesn't acknowledge in time
else if (NetTickDiff(NetTick(), pState->uPingTimer) > 0)
{
NetPrintf(("protohttp2: [%p] peer did not acknowledge ping in time, closing connection\n", pState));
_ProtoHttp2Close(pState, "ping timeout");
pState->bTimeout = TRUE;
}
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2Read
\Description
Read data out of the stream's buffer
\Input *pState - module state
\Input *pStreamInfo - stream we are reading from
\Input *pBuffer - buffer to store data in
\Input iBufMin - minimum number of bytes to return
\Input iBufMax - maximum number of bytes to return (buffer size)
\Output
int32_t - negative=error, zero=no data available or bufmax <= 0, positive=number of bytes read
\Version 11/23/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2Read(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, uint8_t *pBuffer, int32_t iBufMin, int32_t iBufMax)
{
int32_t iLen;
// early out for failure result
if ((pState->eState == ST_FAIL) || ((pStreamInfo->eState == STREAMSTATE_CLOSED) && (pStreamInfo->eErrorType != ERRORTYPE_NO_ERROR)))
{
return(pState->bTimeout ? PROTOHTTP2_TIMEOUT : PROTOHTTP2_RECVFAIL);
}
// if they only wanted head that is what we will return
if ((pStreamInfo->iHeaderLen > 0) && (pStreamInfo->eRequestType == PROTOHTTP_REQUESTTYPE_HEAD))
{
return(PROTOHTTP2_RECVHEAD);
}
// if we haven't sent the headers yet we cannot expect to receive any data
if (pStreamInfo->eState == STREAMSTATE_IDLE)
{
return(PROTOHTTP2_RECVWAIT);
}
// if they are querying only for done state when no more data is available to be read
if ((iBufMax == 0) && ((pStreamInfo->eState == STREAMSTATE_HALF_CLOSED_REMOTE) || (pStreamInfo->eState == STREAMSTATE_CLOSED)) && (pStreamInfo->iBodyReceived == pStreamInfo->iBodySize))
{
return(PROTOHTTP2_RECVDONE);
}
// make sure the range is valid
if (iBufMax < 1)
{
return(0);
}
// clamp the range
iBufMin = DS_MAX(1, iBufMin);
iBufMax = DS_MAX(iBufMin, iBufMax);
iBufMin = DS_MIN(iBufMin, pStreamInfo->iDataMax);
iBufMax = DS_MIN(iBufMax, pStreamInfo->iDataMax);
// figure out how much data is available
iLen = DS_MIN(pStreamInfo->iDataLen, iBufMax);
// check for end of data
if ((iLen == 0) && ((pStreamInfo->eState == STREAMSTATE_HALF_CLOSED_REMOTE) || (pStreamInfo->eState == STREAMSTATE_CLOSED)))
{
// update body size now that stream is complete
pStreamInfo->iBodySize = pStreamInfo->iBodyReceived;
return(PROTOHTTP2_RECVDONE);
}
// see if there is enough to return
if (iLen >= iBufMin)
{
// return data to caller
if (pBuffer != NULL)
{
ds_memcpy(pBuffer, pStreamInfo->pData, iLen);
#if DIRTYCODE_LOGGING
NetPrintfVerbose((pState->iVerbose, 2, "protohttp2: [%p] read %d bytes\n", pState, iLen));
if (pState->iVerbose > 3)
{
NetPrintMem(pBuffer, iLen, "http2-read");
}
#endif
}
pStreamInfo->iBodyReceived += iLen;
memmove(pStreamInfo->pData, pStreamInfo->pData+iLen, pStreamInfo->iDataLen-iLen);
pStreamInfo->iDataLen -= iLen;
// return bytes read
return(iLen);
}
// nothing available
return(0);
}
/*F********************************************************************************/
/*!
\Function _ProtoHttp2EncodeRequest
\Description
Encode the request into the output buffer (headers / data)
\Input *pState - module state
\Input *pStreamInfo - stream we are reading from
\Input *pUrl - address we are sending the request to
\Input *pData - data we are encoding
\Input iDataSize - size of the data we are encoding
\Output
int32_t - negative=error, otherwise amount of user data encoded
\Version 11/23/2016 (eesponda)
*/
/********************************************************************************F*/
static int32_t _ProtoHttp2EncodeRequest(ProtoHttp2RefT *pState, StreamInfoT *pStreamInfo, const char *pUrl, const uint8_t *pData, int32_t iDataSize)
{
int32_t iResult, iOutLen;
char *pOutHdr = NULL;
// save the outlen in case of error
iOutLen = pState->iOutLen;
// format the request headers
if ((iResult = _ProtoHttp2FormatRequestHeader(pState, pStreamInfo, pUrl, iDataSize, &pOutHdr)) < 0)
{
return(iResult);
}
// encode the request headers
if ((iResult = _ProtoHttp2EncodeHeaders(pState, pStreamInfo, iDataSize == 0, pOutHdr)) < 0)
{
NetPrintf(("protohttp2: [%p] not enough room in the output buffer to encode headers\n", pState));
}
// encode the data if any exist
else if ((pData != NULL) && (iDataSize > 0))
{
if ((iResult = _ProtoHttp2EncodeData(pState, pStreamInfo, pData, iDataSize, TRUE)) < 0)
{
pState->iOutLen = iOutLen;
}
}
// cleanup temporary buffer
if (pOutHdr != NULL)
{
DirtyMemFree(pOutHdr, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
}
// return result back to user
return(iResult);
}
/*** Public Functions *************************************************************/
/*F********************************************************************************/
/*!
\Function ProtoHttp2Create
\Description
Allocate module state and prepare for use
\Input iBufSize - length of recv buffer
\Output
ProtoHttp2RefT *- pointer to module state, or NULL
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
ProtoHttp2RefT *ProtoHttp2Create(int32_t iBufSize)
{
ProtoHttp2RefT *pState;
int32_t iMemGroup;
void *pMemGroupUserData;
// query memgroup data
DirtyMemGroupQuery(&iMemGroup, &pMemGroupUserData);
// clamp the buffer size based on the default frame size and maximum frame size
iBufSize = PROTOHTTP2_ClampFrameSize(iBufSize);
// allocate state
if ((pState = (ProtoHttp2RefT *)DirtyMemAlloc(sizeof(*pState), PROTOHTTP_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
{
NetPrintf(("protohttp2: unable to allocate module state\n"));
return(NULL);
}
ds_memclr(pState, sizeof(*pState));
pState->iMemGroup = iMemGroup;
pState->pMemGroupUserData = pMemGroupUserData;
pState->iVerbose = 1;
pState->uTimeout = PROTOHTTP2_TIMEOUT_DEFAULT;
pState->bHuffman = TRUE;
// set the buffer sizes to the defaults, adding the additional space for a frame header
pState->iInpMax = iBufSize+PROTOHTTP2_RESERVED_SIZE;
pState->iOutMax = _ProtoHttp2_DefaultSettings.uMaxFrameSize+PROTOHTTP2_RESERVED_SIZE;
// allocate ssl state
if ((pState->pSsl = ProtoSSLCreate()) == NULL)
{
NetPrintf(("protohttp2: [%p] unable to allocate ssl module state\n", pState));
ProtoHttp2Destroy(pState);
return(NULL);
}
ProtoSSLControl(pState->pSsl, 'alpn', 0, 0, (void *)"h2"); // tell protossl to advertise the http2 protocol
ProtoSSLControl(pState->pSsl, 'snod', TRUE, 0, NULL); // set TCP_NODELAY on the SSL socket
// allocate input buffer
if ((pState->pInpBuf = (uint8_t *)DirtyMemAlloc(pState->iInpMax, PROTOHTTP_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
{
NetPrintf(("protohttp2: [%p] unable to allocate protohttp2 input buffer\n", pState));
ProtoHttp2Destroy(pState);
return(NULL);
}
ds_memclr(pState->pInpBuf, pState->iInpMax);
// allocate output buffer
if ((pState->pOutBuf = (uint8_t *)DirtyMemAlloc(pState->iOutMax, PROTOHTTP_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
{
NetPrintf(("protohttp2: [%p] unable to allocate protohttp2 output buffer\n", pState));
ProtoHttp2Destroy(pState);
return(NULL);
}
ds_memclr(pState->pOutBuf, pState->iOutMax);
// create the encoder context
if ((pState->pEncoder = HpackCreate(_ProtoHttp2_DefaultSettings.uHeaderTableSize, FALSE)) == NULL)
{
NetPrintf(("protohttp2: [%p] unable to create encoder context\n", pState));
ProtoHttp2Destroy(pState);
return(NULL);
}
// create the decoder context
if ((pState->pDecoder = HpackCreate(_ProtoHttp2_DefaultSettings.uHeaderTableSize, TRUE)) == NULL)
{
NetPrintf(("protohttp2: [%p] unable to create decoder context\n", pState));
ProtoHttp2Destroy(pState);
return(NULL);
}
// init crit
NetCritInit(&pState->HttpCrit, "ProtoHttp2");
// reset the state to default
_ProtoHttp2Reset(pState);
// return the state
return(pState);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Update
\Description
Give time to module to do its thing (should be called periodically to
allow module to perform work)
\Input *pState - module state
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2Update(ProtoHttp2RefT *pState)
{
int32_t iResult;
// give time to ssl module
ProtoSSLUpdate(pState->pSsl);
// acquire sole access to http crit
NetCritEnter(&pState->HttpCrit);
// see if the connection is complete
if (pState->eState == ST_CONN)
{
int32_t iStream;
iResult = ProtoSSLStat(pState->pSsl, 'stat', NULL, 0);
if (iResult > 0)
{
/* just use some stack space to send this connection preface instead
of having to juggle with our output buffer (that might have data) */
uint8_t aConnectionPreface[128];
int32_t iOffset = 0;
// add the connection preface to the buffer
ds_memcpy(aConnectionPreface, _ProtoHttp2_ConnectionPreface, sizeof(_ProtoHttp2_ConnectionPreface));
iOffset += sizeof(_ProtoHttp2_ConnectionPreface);
// encode our settings
iOffset += _ProtoHttp2EncodeSettings(pState, FALSE, aConnectionPreface+iOffset, sizeof(aConnectionPreface)-iOffset);
// send the connection preface+settings and wait for settings from server
_ProtoHttp2Send(pState, aConnectionPreface, iOffset);
pState->eState = ST_ACTIVE;
pState->uSettingsTimer = NetTick() + PROTOHTTP2_TIMEOUT_DEFAULT;
}
else if (iResult < 0)
{
NetPrintf(("protohttp2: [%p] ST_CONN got ST_FAIL (err=%d)\n", pState, iResult));
pState->eState = ST_FAIL;
pState->iSslFail = ProtoSSLStat(pState->pSsl, 'fail', NULL, 0);
pState->iHresult = ProtoSSLStat(pState->pSsl, 'hres', NULL, 0);
}
else if (NetTickDiff(NetTick(), pState->uTimer) >= 0)
{
NetPrintf(("protohttp2: [%p] timed out while establishing connection\n", pState));
_ProtoHttp2Close(pState, "timeout");
pState->bTimeout = TRUE;
}
// if a failure occured when establishing the connection handle closing our streams
for (iStream = 0; (pState->eState == ST_FAIL) && (iStream < pState->iNumStreams); iStream += 1)
{
_ProtoHttp2StreamCloseOnError(pState, &pState->Streams[iStream]);
}
}
/* otherwise we are connected so let's try to send / receive data from our peer
we will act on the data depending our state */
else if ((pState->eState > ST_CONN) && (pState->eState < ST_FAIL))
{
// send any data if we have any
if (pState->iOutLen > 0)
{
// send as much data as we can and update our offset accordingly
if ((iResult = _ProtoHttp2Send(pState, pState->pOutBuf+pState->iOutOff, pState->iOutLen-pState->iOutOff)) > 0)
{
pState->iOutOff += iResult;
}
// if we sent all our data, clear our length/offset
if (pState->iOutOff == pState->iOutLen)
{
pState->iOutLen = pState->iOutOff = 0;
}
}
// receive new data from our peer
if ((iResult = _ProtoHttp2Recv(pState, pState->pInpBuf+pState->iInpLen, pState->iInpMax-pState->iInpLen)) > 0)
{
FrameHeaderT Header;
const uint8_t *pBuf;
StreamInfoT *pStreamInfo = NULL;
// increment the offset into the input buffer
pState->iInpLen += iResult;
// decode as many frames as possible
while ((pBuf = _ProtoHttp2DecodeFrameHeader(pState, pState->pInpBuf, pState->iInpLen, &Header, &pStreamInfo)) != NULL)
{
const uint32_t uFrameLength = Header.uLength+Header.uPadding+PROTOHTTP2_HEADER_SIZE;
// handle the frame
_ProtoHttp2HandleFrame(pState, pBuf, &Header, pStreamInfo);
/* advance the buffer past the frame and update new length
note: we need to take advantage of as much space as possible in our buffer.
for that reason we cannot just use an offset into the buffer like we
do on the send side. */
memmove(pState->pInpBuf, pState->pInpBuf+uFrameLength, pState->iInpLen-uFrameLength);
pState->iInpLen -= uFrameLength;
}
}
// handle settings synchronization
_ProtoHttp2CheckSettings(pState);
// check our receive windows and send updates if needed
_ProtoHttp2CheckWindows(pState);
// check and handle if we lack i/o activity
_ProtoHttp2CheckActivityTimeout(pState);
// attempt to handle any connection wide error
if (pState->eErrorType != ERRORTYPE_NO_ERROR)
{
_ProtoHttp2SendGoAway(pState, pState->eErrorType, _ProtoHttp2_strErrorType[pState->eErrorType]);
}
}
// release access to http crit
NetCritLeave(&pState->HttpCrit);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Destroy
\Description
Destroy the module and release its state
\Input *pState - module state
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2Destroy(ProtoHttp2RefT *pState)
{
int32_t iStream;
// try to gracefully close the connection
ProtoHttp2Close(pState);
// clean up append header memory
_ProtoHttp2SetAppendHeader(pState, NULL);
// cleanup stream info memory
for (iStream = 0; iStream < pState->iNumStreams; iStream += 1)
{
_ProtoHttp2StreamInfoCleanup(pState, &pState->Streams[iStream]);
}
NetCritKill(&pState->HttpCrit);
if (pState->pDecoder != NULL)
{
HpackDestroy(pState->pDecoder);
pState->pDecoder = NULL;
}
if (pState->pEncoder != NULL)
{
HpackDestroy(pState->pEncoder);
pState->pEncoder = NULL;
}
if (pState->pInpBuf != NULL)
{
DirtyMemFree(pState->pInpBuf, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
pState->pInpBuf = NULL;
}
if (pState->pOutBuf != NULL)
{
DirtyMemFree(pState->pOutBuf, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
pState->pOutBuf = NULL;
}
if (pState->pSsl != NULL)
{
ProtoSSLDestroy(pState->pSsl);
pState->pSsl = NULL;
}
DirtyMemFree(pState, PROTOHTTP_MEMID, pState->iMemGroup, pState->pMemGroupUserData);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Callback
\Description
Set header callbacks.
\Input *pState - module state
\Input *pCustomHeaderCb - pointer to custom send header callback (may be NULL)
\Input *pReceiveHeaderCb- pointer to recv header callback (may be NULL)
\Input *pUserData - user-supplied callback ref (may be NULL)
\Notes
The ProtoHttpCustomHeaderCbT callback is used to allow customization of
the HTTP header before sending. It is more powerful than the append
header functionality, allowing to make changes to any part of the header
before it is sent. The callback should return a negative code if an error
occurred, zero can be returned if the application wants ProtoHttp to
calculate the header size, or the size of the header can be returned if
the application has already calculated it. The header should *not* be
terminated with the double \\r\\n that indicates the end of the entire
header, as protohttp appends itself.
The ProtoHttpReceiveHeaderCbT callback is used to view the header
immediately on reception. It can be used when the built-in header
cache (retrieved with ProtoHttpStatus('htxt') is too small to hold
the entire header received. It is also possible with this method
to view redirection response headers that cannot be retrieved
normally. This can be important if, for example, the application
wishes to attach new cookies to a redirection response. The
custom response header and custom header callback can be used in
conjunction to implement this type of functionality.
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2Callback(ProtoHttp2RefT *pState, ProtoHttp2CustomHeaderCbT *pCustomHeaderCb, ProtoHttp2ReceiveHeaderCbT *pReceiveHeaderCb, void *pUserData)
{
pState->pCustomHeaderCb = pCustomHeaderCb;
pState->pReceiveHeaderCb = pReceiveHeaderCb;
pState->pCallbackRef = pUserData;
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Request
\Description
Initiate an HTTP transfer without callback. Pass in a URL and the module
starts a transfer from the appropriate server.
\Input *pState - module state
\Input *pUrl - the url to retrieve
\Input *pData - user data to send to server (PUT and POST only)
\Input iDataSize - size of user data to send to server (PUT and POST only)
\Input eRequestType - request type to make
\Input *pStreamId - [out] identifier tied to the request
\Output
int32_t - negative=failure, zero=success, >0=number of data bytes sent (PUT and POST only)
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2Request(ProtoHttp2RefT *pState, const char *pUrl, const uint8_t *pData, int32_t iDataSize, ProtoHttpRequestTypeE eRequestType, int32_t *pStreamId)
{
return(ProtoHttp2RequestCb2(pState, pUrl, pData, iDataSize, eRequestType, pStreamId, NULL, NULL, NULL, NULL));
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2RequestCb
\Description
Initiate an HTTP transfer with write callback. Pass in a URL and the module starts
a transfer from the appropriate server.
\Input *pState - module state
\Input *pUrl - the url to retrieve
\Input *pData - user data to send to server (PUT and POST only)
\Input iDataSize - size of user data to send to server (PUT and POST only)
\Input eRequestType - request type to make
\Input *pStreamId - [out] identifier tied to the request
\Input *pWriteCb - write callback (optional)
\Input *pUserData - write callback user data (optional)
\Output
int32_t - negative=failure, zero=success, >0=number of data bytes sent (PUT and POST only)
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2RequestCb(ProtoHttp2RefT *pState, const char *pUrl, const uint8_t *pData, int32_t iDataSize, ProtoHttpRequestTypeE eRequestType, int32_t *pStreamId, ProtoHttp2WriteCbT *pWriteCb, void *pUserData)
{
return(ProtoHttp2RequestCb2(pState, pUrl, pData, iDataSize, eRequestType, pStreamId, pWriteCb, NULL, NULL, pUserData));
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2RequestCb2
\Description
Initiate an HTTP transfer with callbacks. Pass in a URL and the module starts
a transfer from the appropriate server.
\Input *pState - module state
\Input *pUrl - the url to retrieve
\Input *pData - user data to send to server (PUT and POST only)
\Input iDataSize - size of user data to send to server (PUT and POST only)
\Input eRequestType - request type to make
\Input *pStreamId - [out] identifier tied to the request
\Input *pWriteCb - write callback (optional)
\Input *pCustomHeaderCb - custom header callback (optional)
\Input *pReceiveHeaderCb- receive header callback (optional)
\Input *pUserData - write callback user data (optional)
\Output
int32_t - negative=failure, zero=success, >0=number of data bytes sent (PUT and POST only)
\Version 09/11/2017 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2RequestCb2(ProtoHttp2RefT *pState, const char *pUrl, const uint8_t *pData, int32_t iDataSize, ProtoHttpRequestTypeE eRequestType, int32_t *pStreamId, ProtoHttp2WriteCbT *pWriteCb, ProtoHttp2CustomHeaderCbT *pCustomHeaderCb, ProtoHttp2ReceiveHeaderCbT *pReceiveHeaderCb, void *pUserData)
{
char strKind[8], strHost[sizeof(pState->strHost)];
int32_t iPort, bSecure, iResult = 0;
uint8_t bPortSpecified;
StreamInfoT StreamInfo;
// make sure we can create a new stream
if (pState->iNumStreams >= PROTOHTTP2_MAX_STREAMS)
{
NetPrintf(("protohttp2: [%p] exceeded maximum number of concurrent stream\n", pState));
return(-1);
}
if (pStreamId == NULL)
{
NetPrintf(("protohttp2: [%p] stream identifier output is NULL (required to save identifier)\n", pState));
return(-2);
}
// make sure we can even attempt to fit the frame
if ((iDataSize > 0) && ((uint32_t)iDataSize > pState->LocalSettings.uMaxFrameSize))
{
NetPrintf(("protohttp2: [%p] data size exceeds size of the frame, use streaming instead\n", pState));
return(-3);
}
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] %s %s\n", pState, _ProtoHttp2_strRequestNames[eRequestType], pUrl));
// parse the url for kind, host and port
pUrl = ProtoHttpUrlParse2(pUrl, strKind, sizeof(strKind), strHost, sizeof(strHost), &iPort, &bSecure, &bPortSpecified);
// fill in any missing info (relative url) if available
if (_ProtoHttp2ApplyBaseUrl(pState, strKind, strHost, sizeof(strHost), &iPort, &bSecure, bPortSpecified) != 0)
{
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] %s %s://%s:%d%s\n", pState, _ProtoHttp2_strRequestNames[eRequestType],
bSecure ? "https" : "http", strHost, iPort, pUrl));
}
// are we still connected, connected to same endpoint and have valid stream id?
if ((pState->eState > ST_IDLE) && (pState->eState < ST_FAIL) && (bSecure == pState->bSecure) && (ds_stricmp(strHost, pState->strHost) == 0) && (pState->iStreamId > 0))
{
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] reusing previous connection\n", pState));
}
else
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] request new connection -- url change to %s\n", pState, strHost));
// save new server/port/security state
ds_strnzcpy(pState->strHost, strHost, sizeof(pState->strHost));
pState->iPort = iPort;
pState->bSecure = bSecure;
// send goaway if necessary & close
_ProtoHttp2SendGoAway(pState, ERRORTYPE_NO_ERROR, "new connection");
// reset the state to default
_ProtoHttp2Reset(pState);
// start connect
NetPrintfVerbose((pState->iVerbose, 0, "protohttp2: [%p] connect start (tick=%u)\n", pState, NetTick()));
ProtoSSLConnect(pState->pSsl, pState->bSecure, pState->strHost, 0, pState->iPort);
pState->eState = ST_CONN;
pState->uTimer = NetTick() + pState->uTimeout;
}
// setup the stream info
ds_memclr(&StreamInfo, sizeof(StreamInfo));
StreamInfo.iStreamId = *pStreamId = pState->iStreamId;
StreamInfo.eResponseCode = PROTOHTTP_RESPONSE_PENDING;
StreamInfo.eRequestType = eRequestType;
StreamInfo.pWriteCb = pWriteCb;
StreamInfo.pCustomHeaderCb = pCustomHeaderCb;
StreamInfo.pReceiveHeaderCb = pReceiveHeaderCb;
StreamInfo.pUserData = pUserData;
StreamInfo.iDataMax = StreamInfo.iLocalWindow = PROTOHTTP2_WINDOWSIZE; /* we never update our window size at runtime so assume this value */
StreamInfo.iPeerWindow = pState->PeerSettings.uInitialWindowSize;
NetCritEnter(&pState->HttpCrit);
// attempt to encode request
if ((iResult = _ProtoHttp2EncodeRequest(pState, &StreamInfo, pUrl, pData, iDataSize)) < 0)
{
_ProtoHttp2StreamInfoCleanup(pState, &StreamInfo);
*pStreamId = PROTOHTTP2_INVALID_STREAMID;
}
else
{
// add the stream information for tracking
ds_memcpy(&pState->Streams[pState->iNumStreams], &StreamInfo, sizeof(StreamInfo));
pState->iNumStreams += 1;
// increment to next stream id
pState->iStreamId += 2;
}
NetCritLeave(&pState->HttpCrit);
return(iResult);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Send
\Description
Send data during an ongoing request
\Input *pState - module state
\Input iStreamId - identifier of the stream to send the data on
\Input *pData - pointer to data to send
\Input iDataSize - size of data being sent
\Output
int32_t - negative=PROTOHTTP2_MINBUFF or failure otherwise number of data
bytes sent
\Notes
In the case of PROTOHTTP2_MINBUFF we do not have enough space to encode
the frame header. We need to return a special code due to the fact when sending
a zero sized end stream we need to have a way to tell if it was actually encoded
into the buffer. In this case you should retry to send on the next frame.
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2Send(ProtoHttp2RefT *pState, int32_t iStreamId, const uint8_t *pData, int32_t iDataSize)
{
int32_t iResult = -1;
StreamInfoT *pStreamInfo;
if ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL)
{
// make sure the stream is open to allow us to send data
if (pStreamInfo->eState < STREAMSTATE_OPEN)
{
// not ready to send data yet
return(0);
}
else if ((pStreamInfo->eState != STREAMSTATE_OPEN) && (pStreamInfo->eState != STREAMSTATE_HALF_CLOSED_REMOTE))
{
// we have already finished sending, an error occured
return(-1);
}
// encode the data into the output buffer
NetCritEnter(&pState->HttpCrit);
iResult = _ProtoHttp2EncodeData(pState, pStreamInfo, pData, iDataSize, (iDataSize == PROTOHTTP2_STREAM_END));
NetCritLeave(&pState->HttpCrit);
/* if we don't have enough space to encode the buffer but we are not ending the stream, we can just send zero
bytes written. the only reason we send MINBUFF back is to make sure on zero sized payloads we have a way to
indiciate to try again */
if ((iResult < 0) && (iDataSize != PROTOHTTP2_STREAM_END))
{
iResult = 0;
}
}
return(iResult);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Recv
\Description
Return the actual url data.
\Input *pState - module state
\Input iStreamId - identifier of the stream to recv data from
\Input *pBuffer - buffer to store data in
\Input iBufMin - minimum number of bytes to return
\Input iBufMax - maximum number of bytes to return (buffer size)
\Output
int32_t - negative=error, zero=no data available or bufmax <= 0, positive=number of bytes read
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2Recv(ProtoHttp2RefT *pState, int32_t iStreamId, uint8_t *pBuffer, int32_t iBufMin, int32_t iBufMax)
{
StreamInfoT *pStreamInfo;
int32_t iResult = PROTOHTTP2_RECVFAIL;
if ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL)
{
NetCritEnter(&pState->HttpCrit);
iResult = _ProtoHttp2Read(pState, pStreamInfo, pBuffer, iBufMin, iBufMax);
NetCritLeave(&pState->HttpCrit);
}
return(iResult);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2RecvAll
\Description
Return all of the url data.
\Input *pState - module state
\Input iStreamId - identifier of the stream to recv data from
\Input *pBuffer - buffer to store data in
\Input iBufSize - size of buffer
\Output
int32_t - PROTOHTTP_RECV*, or positive=bytes in response
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2RecvAll(ProtoHttp2RefT *pState, int32_t iStreamId, uint8_t *pBuffer, int32_t iBufSize)
{
StreamInfoT *pStreamInfo;
int32_t iRecvMax = iBufSize-1, iRecvResult = PROTOHTTP2_RECVFAIL;
if ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL)
{
NetCritEnter(&pState->HttpCrit);
// try to receive as much as possible, adding to amount received
while ((iRecvResult = _ProtoHttp2Read(pState, pStreamInfo, pBuffer+pStreamInfo->iRecvSize, 1, iRecvMax-pStreamInfo->iRecvSize)) > 0)
{
pStreamInfo->iRecvSize += iRecvResult;
}
NetCritLeave(&pState->HttpCrit);
}
// check the response code
if (iRecvResult == PROTOHTTP2_RECVDONE)
{
pBuffer[pStreamInfo->iRecvSize] = 0;
iRecvResult = pStreamInfo->iRecvSize;
}
else if ((iRecvResult < 0) && (iRecvResult != PROTOHTTP2_RECVWAIT))
{
// an error occured
NetPrintf(("protohttp2: [%p] error %d receiving response\n", pState, iRecvResult));
}
else if (iRecvResult == 0)
{
iRecvResult = (pStreamInfo->iRecvSize < iRecvMax) ? PROTOHTTP2_RECVWAIT : PROTOHTTP2_RECVBUFF;
}
// return result to caller
return(iRecvResult);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Abort
\Description
Abort current operation, if any.
\Input *pState - module state
\Input iStreamId - identifier of the stream to cancel
\Notes
This will send a RST_STREAM frame to the peer endpoint with the
CANCEL (0x8) error code
\Version 11/03/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2Abort(ProtoHttp2RefT *pState, int32_t iStreamId)
{
StreamInfoT *pStreamInfo;
// find stream id and make sure the stream is open to allow us to send data
if (((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL) && (pStreamInfo->eState > STREAMSTATE_IDLE) && (pStreamInfo->eState < STREAMSTATE_CLOSED))
{
NetCritEnter(&pState->HttpCrit);
_ProtoHttp2PrepareRstStream(pState, pStreamInfo, ERRORTYPE_CANCEL, "cancelling the stream");
NetCritLeave(&pState->HttpCrit);
}
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Close
\Description
Close the connection to the server
\Input *pState - module state
\Notes
This will send a GOAWAY frame to the peer endpoint with the
NO_ERROR (0x0) error code
\Version 11/22/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2Close(ProtoHttp2RefT *pState)
{
// if not connected then nothing left to do
if ((pState->eState == ST_IDLE) || (pState->eState == ST_FAIL))
{
return;
}
_ProtoHttp2SendGoAway(pState, ERRORTYPE_NO_ERROR, "user request");
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Status
\Description
Get status information
\Input *pState - module state
\Input iStreamId - stream identifier used by certain selectors
\Input iSelect - info selector (see Notes)
\Input *pBuffer - [out] storage for selector-specific output
\Input iBufSize - size of buffer
\Output
int32_t - selector specific
\Notes
Selectors are:
\verbatim
SELECTOR RETURN RESULT
'body' negative=failed or pending, else size of body (for 64bit size, get via pBuffer)
'code' negative=no response, else server response code (ProtoHttpResponseE)
'date' returns last-modified data, if available
'done' returns status of request negative=failed, zero=pending or positive=done
'essl' returns protossl error state
'head' returns header size negative=failed or pending, otherwise header size
'host' current host copied to output buffer
'hres' returns hResult containing either the socket error, ssl error, or http status code
'htxt' response header for stream identified by iStreamId via output buffer
'imax' returns size of input buffer
'nstm' returns the number of active streams
'port' returns the current port
'rtxt' most http request header text copied to output buffer for stream
'rtyp' returns the request type for the stream
'strm' returns the state of the stream identified by iStreamId as ProtoHttp2StreamStateE
'time' TRUE if the client timed out the connection, else FALSE
\endverbatim
Unhandled selectors are passed on to ProtoSSL.
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2Status(ProtoHttp2RefT *pState, int32_t iStreamId, int32_t iSelect, void *pBuffer, int32_t iBufSize)
{
const StreamInfoT *pStreamInfo;
// return protossl error state (we cache this since we reset the state when we disconnect an error)
if (iSelect == 'essl')
{
return(pState->iSslFail);
}
// return current host
if (iSelect == 'host')
{
ds_strnzcpy(pBuffer, pState->strHost, iBufSize);
return(0);
}
// return hresult containing either the socket error, ssl error or http status code
if (iSelect == 'hres')
{
// validate stream id, stream information and status code
if ((iStreamId > 0) && ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL) && (pStreamInfo->eResponseCode > 0))
{
return(DirtyErrGetHResult(DIRTYAPI_PROTO_HTTP, pStreamInfo->eResponseCode, (pStreamInfo->eResponseCode >= PROTOHTTP_RESPONSE_CLIENTERROR) ? TRUE : FALSE));
}
else
{
return(pState->iHresult);
}
}
// return size of input buffer
if (iSelect == 'imax')
{
return(pState->iInpMax);
}
// return number of active streams
if (iSelect == 'nstm')
{
return(pState->iNumStreams);
}
// return current port
if (iSelect == 'port')
{
return(pState->iPort);
}
// return timeout indicator
if (iSelect == 'time')
{
return(pState->bTimeout);
}
// attempt to get the stream information for the below selectors where valid stream is required
if ((iStreamId <= 0) || ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) == NULL))
{
// pass down to unhandled selectors with no stream specified to ProtoSSL
return(ProtoSSLStat(pState->pSsl, iSelect, pBuffer, iBufSize));
}
// return response code
if (iSelect == 'code')
{
return(pStreamInfo->eResponseCode);
}
// return the header data
if (iSelect == 'date')
{
return(pStreamInfo->iHdrDate);
}
// done check: negative=failed, zero=pending, positive=done
if (iSelect == 'done')
{
if (pState->eState == ST_FAIL)
{
return(-1);
}
if (pStreamInfo->eState == STREAMSTATE_CLOSED)
{
return(1);
}
return(0);
}
// return the request header text
if (iSelect == 'rtxt')
{
ds_strnzcpy(pBuffer, pStreamInfo->strRequestHeader, iBufSize);
return(0);
}
// return the request type
if (iSelect == 'rtyp')
{
return(pStreamInfo->eRequestType);
}
// return the current state of the stream
if (iSelect == 'strm')
{
return(pStreamInfo->eState);
}
/* check the state:
if failure then nothing is happening, thus nothing left to do
otherwise if we have yet to receive our headers then the remaining data has
not been filled out yet */
if (pState->eState == ST_FAIL)
{
return(-1);
}
if (pStreamInfo->eResponseCode == PROTOHTTP_RESPONSE_PENDING)
{
return(-2);
}
// negative=failed or pending, else size of body (for 64bit size, get via pBuffer)
if (iSelect == 'body')
{
if ((pBuffer != NULL) && (iBufSize == sizeof(pStreamInfo->iBodySize)))
{
ds_memcpy(pBuffer, &pStreamInfo->iBodySize, iBufSize);
}
return((int32_t)pStreamInfo->iBodySize);
}
// return size of the header
if (iSelect == 'head')
{
return(pStreamInfo->iHeaderLen);
}
// return the response header text
if (iSelect == 'htxt')
{
ds_strnzcpy(pBuffer, pStreamInfo->pHeader, iBufSize);
return(0);
}
// unhandled selector
return(-1);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2Control
\Description
ProtoHttp2 control function. Different selectors control different behaviors.
\Input *pState - module state
\Input iStreamId - stream identifier used by certain controls
\Input iControl - control selector (see Notes)
\Input iValue - control specific
\Input iValue2 - control specific
\Input *pValue - control specific
\Output
int32_t - control specific
\Notes
Selectors are:
\verbatim
SELECTOR DESCRIPTION
'apnd' The given buffer will be appended to future headers sent
by ProtoHttp2. Note that the User-Agent and Accept lines
in the default header will be replaced, so if these lines
are desired, they should be supplied to the append header.
'huff' Sets the use of huffman encoding for strings (default=TRUE)
'ires' Resize input buffer
'spam' Sets debug output verbosity (0..n)
'time' Sets ProtoHttp client timeout in milliseconds (default=30s)
\endverbatim
Unhandled selectors are passed on to ProtoSSL.
\Version 09/27/2016 (eesponda)
*/
/*******************************************************************************F*/
int32_t ProtoHttp2Control(ProtoHttp2RefT *pState, int32_t iStreamId, int32_t iControl, int32_t iValue, int32_t iValue2, void *pValue)
{
if (iControl == 'apnd')
{
return(_ProtoHttp2SetAppendHeader(pState, (const char *)pValue));
}
if (iControl == 'huff')
{
pState->bHuffman = (uint8_t)iValue;
return(0);
}
if (iControl == 'ires')
{
// clamp the input buffer size
iValue = PROTOHTTP2_ClampFrameSize(iValue);
// attempt to resize the buffer if necessary
if (((uint32_t)iValue != pState->TempSettings.uMaxFrameSize) && (_ProtoHttp2ResizeInputBuffer(pState, iValue+PROTOHTTP2_RESERVED_SIZE) == 0))
{
// set the temp settings to be sent to our peer
pState->TempSettings.uMaxFrameSize = (uint32_t)iValue;
return(0);
}
return(-1);
}
if (iControl == 'spam')
{
pState->iVerbose = iValue;
// fall through to protossl
}
if (iControl == 'time')
{
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] setting timeout to %d ms\n", pState, iValue));
pState->uTimeout = (unsigned)iValue;
return(0);
}
// unhandled control, fallthrough to protossl
return(ProtoSSLControl(pState->pSsl, iControl, iValue, iValue2, pValue));
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2SetBaseUrl
\Description
Set base url that will be used for any relative url references.
\Input *pState - module state
\Input *pUrl - base url
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2SetBaseUrl(ProtoHttp2RefT *pState, const char *pUrl)
{
char strKind[8];
uint8_t bPortSpecified;
// parse the url for kind, host and port
ProtoHttpUrlParse2(pUrl, strKind, sizeof(strKind), pState->strBaseHost, sizeof(pState->strBaseHost),
&pState->iBasePort, &pState->bBaseSecure, &bPortSpecified);
NetPrintfVerbose((pState->iVerbose, 1, "protohttp2: [%p] setting base url to %s://%s:%d\n",
pState, pState->bBaseSecure ? "https" : "http", pState->strBaseHost, pState->iBasePort));
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2GetLocationHeader
\Description
Get location header from the input buffer. The Location header requires
some special processing.
\Input *pState - reference pointer
\Input *pInpBuf - buffer holding header text
\Input *pBuffer - [out] output buffer for parsed location header, null for size request
\Input iBufSize - size of output buffer, zero for size request
\Input **pHdrEnd- [out] pointer past end of parsed header (optional)
\Output
int32_t - negative=not found or not enough space, zero=success, positive=size query result
\Version 09/27/2016 (eesponda)
*/
/********************************************************************************F*/
int32_t ProtoHttp2GetLocationHeader(ProtoHttp2RefT *pState, const char *pInpBuf, char *pBuffer, int32_t iBufSize, const char **pHdrEnd)
{
const char *pLocHdr;
int32_t iLocLen, iLocPreLen=0;
// get a pointer to header
if ((pLocHdr = ProtoHttpFindHeaderValue(pInpBuf, "location")) == NULL)
{
return(-1);
}
/* according to RFC location headers should be absolute, but some webservers respond with relative
URL's. we assume any url that does not include "://" is a relative url, and if we find one, we
assume the request keeps the same security, port, and host as the previous request */
if ((pState != NULL) && (!strstr(pLocHdr, "://")))
{
char strTemp[288]; // space for max DNS name (253 chars) plus max http url prefix
// format http url prefix
if ((pState->bSecure && (pState->iPort == 443)) || (pState->iPort == 80))
{
ds_snzprintf(strTemp, sizeof(strTemp), "%s://%s", pState->bSecure ? "https" : "http", pState->strHost);
}
else
{
ds_snzprintf(strTemp, sizeof(strTemp), "%s://%s:%d", pState->bSecure ? "https" : "http", pState->strHost, pState->iPort);
}
// make sure relative URL includes '/' as the first character, required when we format the redirection url
if (*pLocHdr != '/')
{
ds_strnzcat(strTemp, "/", sizeof(strTemp));
}
// calculate url prefix length
iLocPreLen = (int32_t)strlen(strTemp);
// copy url prefix text if a buffer is specified
if (pBuffer != NULL)
{
ds_strnzcpy(pBuffer, strTemp, iBufSize);
pBuffer = (char *)((uint8_t *)pBuffer + iLocPreLen);
iBufSize -= iLocPreLen;
}
}
// extract location header and return size
iLocLen = ProtoHttpExtractHeaderValue(pLocHdr, pBuffer, iBufSize, pHdrEnd);
// if it's a size request add in (possible) url header length
if ((pBuffer == NULL) && (iBufSize == 0))
{
iLocLen += iLocPreLen;
}
// return to caller
return(iLocLen);
}
/*F********************************************************************************/
/*!
\Function ProtoHttp2StreamFree
\Description
Removes the stream information for the list by identifier
\Input *pState - module state
\Input iStreamId - identifier of the stream info to remove
\Version 11/01/2016 (eesponda)
*/
/********************************************************************************F*/
void ProtoHttp2StreamFree(ProtoHttp2RefT *pState, int32_t iStreamId)
{
StreamInfoT *pStreamInfo;
if ((pStreamInfo = _ProtoHttp2StreamInfoGet(pState, iStreamId)) != NULL)
{
// get index based on pointer
int32_t iStream = (int32_t)(pStreamInfo - pState->Streams);
#if DIRTYCODE_LOGGING
if (pStreamInfo->eState != STREAMSTATE_CLOSED)
{
NetPrintf(("protohttp2: [%p] warning: freeing stream state for a stream that is not yet closed\n", pState));
}
#endif
// cleanup any dynamic memory
_ProtoHttp2StreamInfoCleanup(pState, pStreamInfo);
// remove entry from stream list
if (iStream != (pState->iNumStreams-1))
{
int32_t iNumMove = (pState->iNumStreams-1) - iStream;
// move the stream info to remove the gap
memmove(pStreamInfo, pStreamInfo+1, iNumMove * sizeof(*pStreamInfo));
}
// decrement count
pState->iNumStreams -= 1;
}
}