-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathHTTPMessage.cpp
More file actions
executable file
·379 lines (322 loc) · 11.8 KB
/
Copy pathHTTPMessage.cpp
File metadata and controls
executable file
·379 lines (322 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
/**
ByteBuffer
HTTPMessage.cpp
Copyright 2011-2025 Ramsey Kant
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "HTTPMessage.h"
#include <algorithm>
#include <string>
#include <format>
#include <memory>
#include <print>
#include <ranges>
#include <cctype> // to std::tolower
#include <charconv>
HTTPMessage::HTTPMessage() : ByteBuffer(4096) {
}
HTTPMessage::HTTPMessage(std::string const& sData) : ByteBuffer(sData.size() + 1) {
putBytes((const uint8_t* const)sData.c_str(), sData.size() + 1);
}
HTTPMessage::HTTPMessage(const uint8_t* pData, uint32_t len) : ByteBuffer(pData, len) {
}
/**
* Put Line
* Append a line (string) to the backing ByteBuffer at the current position
*
* @param str String to put into the ByteBuffer
* @param crlf_end If true (default), end the line with a \r\n
*/
void HTTPMessage::putLine(std::string_view str, bool crlf_end) {
putBytes(reinterpret_cast<const uint8_t*>(str.data()), str.size());
if (crlf_end)
putBytes(reinterpret_cast<const uint8_t*>("\r\n"), 2);
}
/**
* Put Headers
* Write all headers currently in the 'headers' map to the ByteBuffer.
* 'Header: value'
*/
void HTTPMessage::putHeaders() {
for (auto const &[key, value] : headers) {
putLine(std::format("{}: {}", key, value), true);
}
// End with a blank line
putLine();
}
/**
* Get Line
* Retrive the entire contents of a line: string from current position until CR or LF, whichever comes first, then increment the read position
* until it's past the last CR or LF in the line
*
* @return Contents of the line in a string (without CR or LF)
*/
std::string HTTPMessage::getLine() {
const uint32_t startPos = getReadPos();
const uint32_t bufSize = size();
if (startPos >= bufSize)
return "";
// Scan for the first CR or LF without advancing rpos
int32_t crlfPos = -1;
for (uint32_t i = startPos; i < bufSize; i++) {
const char c = getChar(i); // absolute read at index — rpos unchanged
if (c == '\r' || c == '\n') {
crlfPos = static_cast<int32_t>(i);
break;
}
}
// No line terminator found — no complete line available
if (crlfPos < 0) {
// Reset the position to before the last line read that we are now discarding
setReadPos(startPos);
return "";
}
// Copy line content into our HTTPMessage ByteBuffer
uint32_t lineLen = static_cast<uint32_t>(crlfPos) - startPos;
std::string ret(lineLen, '\0');
if (lineLen > 0)
getBytes(reinterpret_cast<uint8_t*>(ret.data()), lineLen);
// rpos is now at crlfPos
// Consume up to 2 CR/LF bytes (\r\n as a pair) without skipping a following blank line
uint32_t k = 0;
while (getReadPos() < bufSize && k < 2) {
const auto c = static_cast<char>(peek());
if (c != '\r' && c != '\n')
break;
get();
k++;
}
return ret;
}
/**
* getStrElement
* Get a token from the current buffer, stopping at the delimiter. Returns the token as a string
*
* @param delim The delimiter to stop at when retriving the element. By default, it's a space
* @return Token found in the buffer. Empty if delimiter wasn't reached
*/
std::string HTTPMessage::getStrElement(char delim) {
int32_t startPos = getReadPos();
if (startPos < 0)
return "";
int32_t endPos = find(delim, startPos);
if (endPos < 0)
return "";
if (startPos > endPos)
return "";
// Token spans [startPos, endPos); delimiter sits at endPos
auto tokenLen = static_cast<uint32_t>(endPos - startPos);
if (tokenLen == 0)
return "";
// Grab the token bytes (excludes delimiter), then step past the delimiter
auto str = std::make_unique<char[]>(tokenLen + 1);
getBytes(reinterpret_cast<uint8_t*>(str.get()), tokenLen);
str[tokenLen] = '\0';
std::string ret(str.get(), tokenLen);
// Advance the read position past the delimiter
setReadPos(static_cast<uint32_t>(endPos) + 1);
return ret;
}
/**
* Parse Headers
* When an HTTP message (request & response) has reached the point where headers are present, this method
* should be called to parse and populate the internal map of headers.
* Parse headers will move the read position past the blank line that signals the end of the headers
*/
bool HTTPMessage::parseHeaders() {
constexpr uint32_t MAX_HEADERS = 128;
constexpr uint32_t MAX_MULTILINE_SIZE = 16384; // 16 KB cap on accumulated multiline header value
uint32_t header_count = 0;
std::string hline = getLine();
// Keep pulling headers until a blank line has been reached (signaling the end of headers)
while (!hline.empty()) {
if (++header_count > MAX_HEADERS) {
parseErrorStr = "Too many headers";
return false;
}
// Case where values are on multiple lines ending with a comma
uint32_t accumulated = hline.size();
while (!hline.empty() && hline.back() == ',') {
std::string app = getLine();
accumulated += app.size();
if (accumulated > MAX_MULTILINE_SIZE) {
parseErrorStr = "Multiline header value exceeds maximum size";
return false;
}
hline += app;
}
addHeader(hline);
hline = getLine();
}
return true;
}
/**
* Parse Body
* Parses everything after the headers section of an HTTP message. Handles chuncked responses/requests
*
* @return True if successful. False on error, parseErrorStr is set with a reason
*/
bool HTTPMessage::parseBody() {
// Content-Length should exist (size of the Body data) if there is body data
std::string hlenstr = "";
hlenstr = getHeaderValue("Content-Length");
// No body data to read:
if (hlenstr.empty())
return true;
uint32_t remainingLen = bytesRemaining();
uint32_t contentLen = 0;
// Validate Content-Length is an integer
auto [ptr, ec] = std::from_chars(hlenstr.data(), hlenstr.data() + hlenstr.size(), contentLen);
if (ec != std::errc{}) {
parseErrorStr = std::format("Invalid Content-Length value: {}", hlenstr);
this->dataLen = 0;
return false;
}
constexpr uint32_t MAX_CONTENT_LENGTH = 256u * 1024u * 1024u; // 256 MB
if (contentLen > MAX_CONTENT_LENGTH) {
parseErrorStr = std::format("Content-Length {} exceeds maximum allowed size", contentLen);
this->dataLen = 0;
return false;
}
// contentLen should NOT exceed the remaining number of bytes in the buffer
// Add 1 to bytesRemaining so it includes the byte at the current read position
if (static_cast<uint64_t>(contentLen) > static_cast<uint64_t>(remainingLen) + 1) {
// If it exceeds, there's a potential security issue and we can't reliably parse
parseErrorStr = std::format("Content-Length ({}) is greater than remaining bytes ({})", hlenstr, remainingLen);
this->dataLen = 0;
return false;
} else if (remainingLen > contentLen) {
parseErrorStr = std::format("ByteBuffer remaining size to read ({}) is greater than provided Content-Length {}", remainingLen, contentLen);
this->dataLen = 0;
return false;
} else if (contentLen == 0) {
// Nothing to read, which is fine
return true;
} else {
// Otherwise, we can probably trust Content-Length is valid and read the specificed number of bytes
this->dataLen = contentLen;
}
// Create a big enough buffer to store the data and read from the current position
this->data = std::make_unique<uint8_t[]>(this->dataLen);
getBytes(this->data.get(), this->dataLen);
// We could handle chunked Request/Response parsing (with footers) here, but, we won't.
return true;
}
/**
* Add Header to the Map from string
* Takes a formatted header string "Header: value", parse it, and put it into the std::map as a key,value pair.
*
* @param string containing formatted header: value
*/
void HTTPMessage::addHeader(std::string_view line) {
size_t kpos = line.find(':');
if (kpos == std::string_view::npos) {
std::print("Could not addHeader: {}\n", line);
return;
}
// We're choosing to reject HTTP Header keys longer than 32 characters
if (kpos > 32)
return;
std::string_view key = line.substr(0, kpos);
if (key.empty())
return;
int32_t value_len = line.size() - kpos - 1;
if (value_len <= 0)
return;
// We're choosing to reject HTTP header values longer than 4kb
if (value_len > 4096)
return;
std::string_view value = line.substr(kpos + 1, value_len);
// Skip all leading spaces in the value
int32_t i = 0;
while (i < static_cast<int32_t>(value.size()) && value[i] == 0x20) {
i++;
}
value = value.substr(i);
if (value.empty())
return;
// Add header to the map
addHeader(key, value);
}
/**
* Add header key-value std::pair to the map
*
* @param key String representation of the Header Key
* @param value String representation of the Header value
*/
void HTTPMessage::addHeader(std::string_view key, std::string_view value) {
auto key_lower = key
| std::views::transform([](unsigned char c){ return std::tolower(c); })
| std::ranges::to<std::string>();
headers.try_emplace(std::move(key_lower), std::string(value));
}
/**
* Add header key-value std::pair to the map (Integer value)
* Integer value is converted to a string
*
* @param key String representation of the Header Key
* @param value Integer representation of the Header value
*/
void HTTPMessage::addHeader(std::string_view key, int32_t value) {
auto key_lower = key
| std::views::transform([](unsigned char c){ return std::tolower(c); })
| std::ranges::to<std::string>();
headers.try_emplace(std::move(key_lower), std::format("{}", value));
}
/**
* Get Header Value
* Given a header name (key), return the value associated with it in the headers map
*
* @param key Key to identify the header
*/
std::string HTTPMessage::getHeaderValue(std::string_view key) const {
// All keys are stored lowercase (normalized at insertion), so lowercase the lookup key once
auto key_lower = key
| std::views::transform([](unsigned char c){ return std::tolower(c); })
| std::ranges::to<std::string>();
auto it = headers.find(key_lower);
return (it != headers.end()) ? it->second : "";
}
/**
* Get Header String
* Get the full formatted header string "Header: value" from the headers map at position index
*
* @param index Position in the headers map to retrieve a formatted header string
* @ret Formatted string with header name and value
*/
std::string HTTPMessage::getHeaderStr(int32_t index) const {
int32_t i = 0;
std::string ret = "";
for (auto const &[key, value] : headers) {
if (i == index) {
ret = std::format("{}: {}", key, value);
break;
}
i++;
}
return ret;
}
/**
* Get Number of Headers
* Return the number of headers in the headers map
*
* @return size of the map
*/
uint32_t HTTPMessage::getNumHeaders() const {
return headers.size();
}
/**
* Clear Headers
* Removes all headers from the internal map
*/
void HTTPMessage::clearHeaders() {
headers.clear();
}