// Message buffers available to clients
// This enum must remain synchronized with Http2Api::classic_buffer_names[]
-enum HTTP2_BUFFER { HTTP2_BUFFER_FRAME_HEADER = 1, HTTP2_BUFFER_FRAME_DATA, HTTP2_BUFFER_DECODED_HEADER,
- HTTP2_BUFFER_MAX };
+enum HTTP2_BUFFER { HTTP2_BUFFER_FRAME_HEADER = 1, HTTP2_BUFFER_FRAME_DATA,
+ HTTP2_BUFFER_DECODED_HEADER, HTTP2_BUFFER__MAX };
// Peg counts
// This enum must remain synchronized with Http2Module::peg_names[] in http2_tables.cc
-enum PEG_COUNT { PEG_CONCURRENT_SESSIONS = 0, PEG_MAX_CONCURRENT_SESSIONS, PEG_FLOW,
- PEG_COUNT_MAX };
+enum PEG_COUNT { PEG_FLOW = 0, PEG_CONCURRENT_SESSIONS, PEG_MAX_CONCURRENT_SESSIONS,
+ PEG_MAX_ENTRIES, PEG_COUNT__MAX };
enum EventSid
{
EVENT_SETTINGS_FRAME_ERROR = 11,
EVENT_SETTINGS_FRAME_UNKN_PARAM = 12,
EVENT_FRAME_SEQUENCE = 13,
+ EVENT_DYNAMIC_TABLE_OVERFLOW = 14,
EVENT__MAX_VALUE
};
INF_INVALID_SETTINGS_FRAME = 18,
INF_SETTINGS_FRAME_UNKN_PARAM = 19,
INF_FRAME_SEQUENCE = 20,
+ INF_INVALID_TABLE_SIZE_UPDATE = 21,
+ INF_DYNAMIC_TABLE_OVERFLOW = 22,
+ INF_TABLE_SIZE_UPDATE_WITHIN_HEADER = 23,
+ INF_TOO_MANY_TABLE_SIZE_UPDATES = 24,
INF__MAX_VALUE
};
Http2FlowData::Http2FlowData(Flow* flow_) :
FlowData(inspector_id),
flow(flow_),
- hi((HttpInspect*)(flow->assistant_gadget))
+ hi((HttpInspect*)(flow->assistant_gadget)),
+ hpack_decoder{Http2HpackDecoder(this, SRC_CLIENT), Http2HpackDecoder(this, SRC_SERVER)}
{
if (hi != nullptr)
{
};
class Http2Stream* get_current_stream(const HttpCommon::SourceId source_id);
+ Http2HpackDecoder* get_hpack_decoder(const HttpCommon::SourceId source_id)
+ { return &hpack_decoder[source_id]; }
+ Http2ConnectionSettings* get_connection_settings(const HttpCommon::SourceId source_id)
+ { return &connection_settings[source_id]; }
protected:
snort::Flow* flow;
HttpInspect* const hi;
uint32_t partial_bytes_written;
Field name, value;
+ table_size_update_allowed = false;
+
// Indexed field name
if (encoded_header_buffer[0] & name_index_mask)
{
}
if (with_indexing)
- decode_table.add_index(name, value);
-
+ {
+ // Adding the entry to the dynamic table fails if the number of entries in the dynamic
+ // table exceeds the Snort hard-coded limit of 512
+ if (!decode_table.add_index(name, value))
+ {
+ infractions += INF_DYNAMIC_TABLE_OVERFLOW;
+ events->create_event(EVENT_DYNAMIC_TABLE_OVERFLOW);
+ return false;
+ }
+ }
return true;
}
bytes_written = 0;
bytes_consumed = 0;
+ table_size_update_allowed = false;
+
if (!decode_int.translate(encoded_header_buffer, encoded_header_length, bytes_consumed,
index, events, infractions))
return false;
return true;
}
-// FIXIT-M Will be updated to actually update dynamic table size. For now just skips over
bool Http2HpackDecoder::handle_dynamic_size_update(const uint8_t* encoded_header_buffer,
const uint32_t encoded_header_length, const Http2HpackIntDecode &decode_int,
uint32_t &bytes_consumed, uint32_t &bytes_written)
{
return false;
}
-#ifdef REG_TEST
- //FIXIT-M remove when dynamic size updates are handled
- if (HttpTestManager::use_test_output(HttpTestManager::IN_HTTP2))
+ bytes_consumed += encoded_bytes_consumed;
+
+ if (!table_size_update_allowed)
+ {
+ *infractions += INF_TABLE_SIZE_UPDATE_WITHIN_HEADER;
+ events->create_event(EVENT_MISFORMATTED_HTTP2);
+ return true;
+ }
+ if (num_table_size_updates >= 2)
{
- fprintf(HttpTestManager::get_output_file(),
- "Skipping HPACK dynamic size update: %lu\n", decoded_int);
+ *infractions += INF_TOO_MANY_TABLE_SIZE_UPDATES;
+ events->create_event(EVENT_MISFORMATTED_HTTP2);
+ return true;
}
-#endif
- bytes_consumed += encoded_bytes_consumed;
+
+ if (!decode_table.hpack_table_size_update(decoded_int))
+ {
+ *infractions += INF_INVALID_TABLE_SIZE_UPDATE;
+ events->create_event(EVENT_MISFORMATTED_HTTP2);
+ }
+
+ num_table_size_updates++;
return true;
}
LITERAL_NO_INDEX_NAME_INDEX_MASK, decode_int4, false, bytes_consumed,
decoded_header_buffer, decoded_header_length, bytes_written);
else
- // FIXIT-M dynamic table size update not yet supported, just skip
return handle_dynamic_size_update(encoded_header_buffer,
encoded_header_length, decode_int5, bytes_consumed, bytes_written);
}
infractions = stream_infractions;
pseudo_headers_fragment_size = 0;
+ // A maximum of two table size updates are allowed, and must be at the start of the header block
+ table_size_update_allowed = true;
+ num_table_size_updates = 0;
+
while (success and total_bytes_consumed < encoded_headers_length)
{
success = decode_header_line(encoded_headers + total_bytes_consumed,
class Http2HpackDecoder
{
public:
- Http2HpackDecoder() { }
+ Http2HpackDecoder(Http2FlowData* flow_data, HttpCommon::SourceId src_id) :
+ decode_table(flow_data, src_id) { }
bool decode_headers(const uint8_t* encoded_headers, const uint32_t encoded_headers_length,
uint8_t* decoded_headers, Http2StartLine* start_line,
Http2EventGen* stream_events, Http2Infractions* stream_infractions, bool no_message_body);
bool finalize_start_line();
const Field* get_start_line();
const Field* get_decoded_headers(const uint8_t* const decoded_headers);
+ HpackIndexTable* get_decode_table() { return &decode_table; }
private:
Http2StartLine* start_line = nullptr;
static Http2HpackStringDecode decode_string;
HpackIndexTable decode_table;
+ bool table_size_update_allowed = true;
+ uint8_t num_table_size_updates = 0;
};
#endif
#endif
#include "http2_hpack_dynamic_table.h"
+#include "http2_module.h"
#include <string.h>
#include "http2_hpack_table.h"
+using namespace Http2Enums;
+
HpackDynamicTable::~HpackDynamicTable()
{
- assert(num_entries <= array_capacity);
- const uint32_t end_index = (start + num_entries) % array_capacity;
- for (uint32_t i = 0; i < array_capacity; i++)
- {
- if ((start <= end_index and (i >= start and i < end_index)) or
- (start > end_index and (i >= start or i < end_index)))
- {
- delete circular_array[i];
- circular_array[i] = nullptr;
- }
- }
+ for (unsigned i = 0; i < ARRAY_CAPACITY; i++)
+ delete circular_array[i];
delete[] circular_array;
}
-void HpackDynamicTable::add_entry(Field name, Field value)
+bool HpackDynamicTable::add_entry(const Field& name, const Field& value)
{
+ // The add only fails if the underlying circular array is out of space
+ if (num_entries >= ARRAY_CAPACITY)
+ return false;
+
const uint32_t new_entry_size = name.length() + value.length() + RFC_ENTRY_OVERHEAD;
// As per the RFC, attempting to add an entry that is larger than the max size of the table is
if (new_entry_size > max_size)
{
prune_to_size(0);
- return;
+ return true;
}
+ // Create new entry. This is done before pruning because the entry referenced by the new name
+ // may be pruned.
+ HpackTableEntry *new_entry = new HpackTableEntry(name, value);
+
// If add entry would exceed max table size, evict old entries
prune_to_size(max_size - new_entry_size);
// Add new entry to the front of the table (newest entry = lowest index)
- HpackTableEntry *new_entry = new HpackTableEntry(name, value);
-
- start = (start + array_capacity - 1) % array_capacity;
-
- // FIXIT-P May want to initially allocate small circular array and expand as needed. For now
- // array big enough to support hardcoded max table size of 4096 bytes
- assert(num_entries < array_capacity);
+ start = (start + ARRAY_CAPACITY - 1) % ARRAY_CAPACITY;
circular_array[start] = new_entry;
num_entries++;
+ if (num_entries > Http2Module::get_peg_counts(PEG_MAX_ENTRIES))
+ Http2Module::increment_peg_counts(PEG_MAX_ENTRIES);
+
rfc_table_size += new_entry_size;
+ return true;
}
const HpackTableEntry* HpackDynamicTable::get_entry(uint32_t virtual_index) const
{
const uint32_t dyn_index = virtual_index - HpackIndexTable::STATIC_MAX_INDEX - 1;
- if (num_entries == 0 or dyn_index > num_entries - 1)
+ if (dyn_index + 1 > num_entries)
return nullptr;
- const uint32_t arr_index = (start + dyn_index) % array_capacity;
+ const uint32_t arr_index = (start + dyn_index) % ARRAY_CAPACITY;
return circular_array[arr_index];
}
* until the new entry fits. If the dynamic size update is smaller than the current table size,
* entries are pruned until the table is no larger than the max size. Entries are pruned least
* recently added first.
- * Note: dynamic size updates not yet implemented
*/
void HpackDynamicTable::prune_to_size(uint32_t new_max_size)
{
while (rfc_table_size > new_max_size)
{
- const uint32_t last_index = (start + num_entries - 1 + array_capacity) % array_capacity;
- HpackTableEntry *last_entry = circular_array[last_index];
+ const uint32_t last_index = (start + num_entries - 1 + ARRAY_CAPACITY) % ARRAY_CAPACITY;
num_entries--;
- rfc_table_size -= last_entry->name.length() + last_entry->value.length() +
- RFC_ENTRY_OVERHEAD;
- delete last_entry;
+ rfc_table_size -= circular_array[last_index]->name.length() +
+ circular_array[last_index]->value.length() + RFC_ENTRY_OVERHEAD;
+ delete circular_array[last_index];
circular_array[last_index] = nullptr;
}
}
+
+void HpackDynamicTable::update_size(uint32_t new_size)
+{
+ if (new_size < rfc_table_size)
+ {
+ prune_to_size(new_size);
+ }
+ max_size = new_size;
+}
class HpackDynamicTable
{
public:
- // FIXIT-M allocate array based on actual max_size from settings
- HpackDynamicTable() : circular_array(new HpackTableEntry*[DEFAULT_NUM_ENTRIES]()) { }
+ // FIXIT-P This array can be optimized to start smaller and grow on demand
+ HpackDynamicTable() : circular_array(new HpackTableEntry*[ARRAY_CAPACITY]()) { }
~HpackDynamicTable();
const HpackTableEntry* get_entry(uint32_t index) const;
- void add_entry(Field name, Field value);
- void prune_to_size(uint32_t new_max_size);
- // FIXIT-M implement handle_dynamic_size_update function
+ bool add_entry(const Field& name, const Field& value);
+ void update_size(uint32_t new_size);
+ uint32_t get_max_size() { return max_size; }
+
private:
+ void expand_array();
+
const static uint32_t RFC_ENTRY_OVERHEAD = 32;
- // FIXIT-H set/update these parameters dynamically. For now hardcoded
const static uint32_t DEFAULT_MAX_SIZE = 4096;
- const static uint32_t DEFAULT_NUM_ENTRIES = DEFAULT_MAX_SIZE / RFC_ENTRY_OVERHEAD;
- uint32_t array_capacity = DEFAULT_NUM_ENTRIES;
+ const static uint32_t ARRAY_CAPACITY = 512;
uint32_t max_size = DEFAULT_MAX_SIZE;
uint32_t start = 0;
uint32_t num_entries = 0;
uint32_t rfc_table_size = 0;
HpackTableEntry** circular_array;
+
+ void prune_to_size(uint32_t new_max_size);
};
#endif
//--------------------------------------------------------------------------
// http2_hpack_table.cc author Katura Harvey <katharve@cisco.com>
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "http2_enum.h"
+#include "http2_flow_data.h"
#include "http2_hpack_table.h"
#include <string.h>
#define MAKE_TABLE_ENTRY(name, value) \
HpackTableEntry(strlen(name), (const uint8_t*)name, strlen(value), (const uint8_t*)value)
+using namespace Http2Enums;
+
HpackTableEntry::HpackTableEntry(const Field& copy_name, const Field& copy_value)
{
uint8_t* new_name = new uint8_t[copy_name.length()];
uint8_t* new_value = new uint8_t[copy_value.length()];
- memcpy(new_value, copy_value.start(), copy_value.length());
memcpy(new_name, copy_name.start(), copy_name.length());
+ memcpy(new_value, copy_value.start(), copy_value.length());
name.set(copy_name.length(), new_name, true);
value.set(copy_value.length(), new_value, true);
else
return dynamic_table.get_entry(index);
}
+
+bool HpackIndexTable::add_index(const Field& name, const Field& value)
+{
+ return dynamic_table.add_entry(name, value);
+}
+
+void HpackIndexTable::settings_table_size_update(uint32_t new_size)
+{
+ if (!encoder_set_max_size)
+ dynamic_table.update_size(new_size);
+ else if (new_size < dynamic_table.get_max_size())
+ {
+ encoder_set_max_size = false;
+ dynamic_table.update_size(new_size);
+ }
+}
+
+// A dynamic table size update sent in an HPACK encoder cannot be larger than last
+// HEADER_TABLE_SIZE settings frame parameter sent by the decoder
+bool HpackIndexTable::hpack_table_size_update(uint32_t new_size)
+{
+ encoder_set_max_size = true;
+ if (new_size <= session_data->get_connection_settings((HttpCommon::SourceId)(1 - source_id))->
+ get_param(HEADER_TABLE_SIZE))
+ {
+ dynamic_table.update_size(new_size);
+ return true;
+ }
+ else
+ return false;
+}
#include "http2_enum.h"
#include "http2_hpack_dynamic_table.h"
+class Http2FlowData;
+
struct HpackTableEntry
{
HpackTableEntry(uint32_t name_len, const uint8_t* _name, uint32_t value_len,
class HpackIndexTable
{
public:
+ HpackIndexTable(Http2FlowData* flow_data, HttpCommon::SourceId src_id) :
+ session_data(flow_data), source_id(src_id) { }
const HpackTableEntry* lookup(uint64_t index) const;
- void add_index(Field name, Field value) { dynamic_table.add_entry(name, value); }
+ bool add_index(const Field& name, const Field& value);
+ bool hpack_table_size_update(const uint32_t size);
+ void settings_table_size_update(const uint32_t size);
const static uint8_t STATIC_MAX_INDEX = 61;
const static uint8_t PSEUDO_HEADER_MAX_STATIC_INDEX = 14;
private:
const static HpackTableEntry static_table[STATIC_MAX_INDEX + 1];
HpackDynamicTable dynamic_table;
+ Http2FlowData* session_data;
+ HttpCommon::SourceId source_id;
+ bool encoder_set_max_size = false;
};
#endif
// The current frame now owns these buffers, clear them from the flow data
session_data->frame_header[source_id] = nullptr;
+ session_data->frame_header_size[source_id] = 0;
session_data->frame_data[source_id] = nullptr;
+ session_data->frame_data_size[source_id] = 0;
session_data->frame_in_detection = true;
ProfileStats* Http2Module::get_profile() const
{ return &http2_profile; }
-THREAD_LOCAL PegCount Http2Module::peg_counts[PEG_COUNT_MAX] = { 0 };
+THREAD_LOCAL PegCount Http2Module::peg_counts[PEG_COUNT__MAX] = { 0 };
bool Http2Module::begin(const char*, int, SnortConfig*)
{
continue;
}
+ handle_update(parameter_id, parameter_value);
session_data->connection_settings[source_id].set_param(parameter_id, parameter_value);
}
}
return !(bad_frame);
}
+void Http2SettingsFrame::handle_update(uint16_t id, uint32_t value)
+{
+ switch (id)
+ {
+ case HEADER_TABLE_SIZE:
+ // Sending a table size parameter informs the receiver the maximum hpack dynamic
+ // table size they may use.
+ session_data->get_hpack_decoder((HttpCommon::SourceId) (1 - source_id))->
+ get_decode_table()->settings_table_size_update(value);
+ break;
+ default:
+ break;
+ }
+}
+
#ifdef REG_TEST
void Http2SettingsFrame::print_frame(FILE* output)
{
void parse_settings_frame();
bool sanity_check();
+ void handle_update(uint16_t id, uint32_t value);
bool bad_frame = false;
static const uint8_t SfAck = 0x01;
if (stream)
http_flow = (HttpFlowData*)stream->get_hi_flow_data();
- if (!stream || !http_flow ||
- (http_flow->get_type_expected(source_id) != HttpEnums::SEC_BODY_CHUNK))
+ if (!stream || !http_flow || (frame_length > 0 and
+ (http_flow->get_type_expected(source_id) != HttpEnums::SEC_BODY_CHUNK)))
{
*session_data->infractions[source_id] += INF_FRAME_SEQUENCE;
session_data->events[source_id]->create_event(EVENT_FRAME_SEQUENCE);
{ EVENT_SETTINGS_FRAME_ERROR, "error in HTTP/2 settings frame" },
{ EVENT_SETTINGS_FRAME_UNKN_PARAM, "unknown parameter in HTTP/2 settings frame" },
{ EVENT_FRAME_SEQUENCE, "invalid HTTP/2 frame sequence" },
+ { EVENT_DYNAMIC_TABLE_OVERFLOW, "HTTP/2 dynamic table size limit exceeded" },
{ 0, nullptr }
};
-const PegInfo Http2Module::peg_names[PEG_COUNT_MAX+1] =
+const PegInfo Http2Module::peg_names[PEG_COUNT__MAX+1] =
{
{ CountType::SUM, "flows", "HTTP connections inspected" },
{ CountType::NOW, "concurrent_sessions", "total concurrent HTTP/2 sessions" },
{ CountType::MAX, "max_concurrent_sessions", "maximum concurrent HTTP/2 sessions" },
+ { CountType::MAX, "max_table_entries", "maximum entries in an HTTP/2 dynamic table" },
{ CountType::END, nullptr, nullptr }
};