Skip to content

Commit e271cb9

Browse files
committed
structured headers instead of plain text formatting
1 parent 83013ca commit e271cb9

1 file changed

Lines changed: 239 additions & 55 deletions

File tree

code/espurna/libs/Http.h

Lines changed: 239 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,163 @@
88
#define ASYNC_HTTP_DEBUG(...) //DEBUG_PORT.printf(__VA_ARGS__)
99
#endif
1010

11-
// TODO: customizable headers
12-
// <method> <path> <host> <len>
13-
const char HTTP_REQUEST_TEMPLATE[] PROGMEM =
14-
"%s %s HTTP/1.1\r\n"
15-
"Host: %s\r\n"
16-
"User-Agent: ESPurna\r\n"
17-
"Connection: close\r\n"
18-
"Content-Type: application/x-www-form-urlencoded\r\n"
19-
"Content-Length: %u\r\n"
20-
"\r\n";
11+
namespace Headers {
12+
PROGMEM const char HOST[] = "Host";
13+
PROGMEM const char USER_AGENT[] = "User-Agent";
14+
PROGMEM const char CONNECTION[] = "Connection";
15+
PROGMEM const char CONTENT_TYPE[] = "Content-Type";
16+
PROGMEM const char CONTENT_LENGTH[] = "Content-Length";
17+
};
18+
19+
struct AsyncHttpHeader {
20+
21+
using header_t = std::pair<const String&, const String&>;
22+
23+
private:
24+
25+
const String _key;
26+
const String _value;
27+
header_t _kv;
28+
29+
public:
30+
31+
AsyncHttpHeader(const char* key, const char* value) :
32+
_key(FPSTR(key)),
33+
_value(FPSTR(value)),
34+
_kv(_key, _value)
35+
{}
36+
37+
AsyncHttpHeader(const String& key, const String& value) :
38+
_key(key),
39+
_value(value),
40+
_kv(_key, _value)
41+
{}
42+
43+
AsyncHttpHeader(const AsyncHttpHeader& other) :
44+
_key(other._key),
45+
_value(other._value),
46+
_kv(_key, _value)
47+
{}
48+
49+
const header_t& get() const {
50+
return _kv;
51+
}
52+
53+
const char* key() const {
54+
return _key.c_str();
55+
}
56+
57+
const char* value() const {
58+
return _value.c_str();
59+
}
60+
61+
size_t keyLength() const {
62+
return _key.length();
63+
}
64+
65+
size_t valueLength() const {
66+
return _value.length();
67+
}
68+
69+
bool operator ==(const AsyncHttpHeader& header) {
70+
return (
71+
(header._key == _key) && (header._value == _value)
72+
);
73+
}
74+
75+
};
76+
77+
struct AsyncHttpHeaders {
78+
79+
using header_t = AsyncHttpHeader;
80+
using headers_t = std::vector<header_t>;
81+
82+
private:
83+
84+
headers_t _headers;
85+
size_t _index;
86+
size_t _last;
87+
String _value;
88+
89+
public:
90+
91+
AsyncHttpHeaders() :
92+
_index(0),
93+
_last(std::numeric_limits<size_t>::max())
94+
{}
95+
96+
AsyncHttpHeaders(headers_t& headers) :
97+
_headers(headers),
98+
_index(0),
99+
_last(std::numeric_limits<size_t>::max())
100+
{}
101+
102+
void add(const header_t& header) {
103+
_headers.push_back(header);
104+
}
105+
106+
size_t size() {
107+
return _headers.size();
108+
}
109+
110+
void reserve(size_t size) {
111+
_headers.reserve(size);
112+
}
113+
114+
bool has(const char* key) {
115+
for (const auto& header : _headers) {
116+
if (strcmp_P(key, header.key()) == 0) return true;
117+
}
118+
return false;
119+
}
120+
121+
String& current() {
122+
if (_last == _index) return _value;
123+
if (_headers.size() && (_index < _headers.size())) {
124+
const auto& current = _headers.at(_index);
125+
_value.reserve(
126+
current.keyLength()
127+
+ current.valueLength()
128+
+ strlen(": \r\n")
129+
);
130+
131+
_value = current.key();
132+
_value += ": ";
133+
_value += current.value();
134+
_value += "\r\n";
135+
} else {
136+
_value = "";
137+
}
138+
139+
_last = _index;
140+
141+
return _value;
142+
}
143+
144+
String& next() {
145+
++_index;
146+
return current();
147+
}
148+
149+
bool done() {
150+
return (_index >= _headers.size());
151+
}
152+
153+
void clear() {
154+
_index = 0;
155+
_last = std::numeric_limits<size_t>::max();
156+
_headers.clear();
157+
}
158+
159+
headers_t::const_iterator begin() {
160+
return _headers.begin();
161+
}
162+
163+
headers_t::const_iterator end() {
164+
return _headers.end();
165+
}
166+
167+
};
21168

22169
struct AsyncHttpError {
23170

@@ -53,6 +200,9 @@ struct AsyncHttpError {
53200

54201
class AsyncHttp {
55202

203+
constexpr const size_t DEFAULT_TIMEOUT = 5000;
204+
constexpr const size_t DEFAULT_PATH_BUFSIZE = 256;
205+
56206
public:
57207

58208
AsyncClient client;
@@ -92,18 +242,23 @@ class AsyncHttp {
92242
String method;
93243
String path;
94244

245+
// WebRequest.cpp
246+
//LinkedList<AsyncWebHeader*> headers;
247+
//std::vector<AsyncHttpHeader> headers;
248+
AsyncHttpHeaders headers;
249+
95250
String host;
96251
uint16_t port;
97252

98253
uint32_t ts;
99-
uint32_t timeout = 5000;
254+
uint32_t timeout = DEFAULT_TIMEOUT;
100255

101256
bool connected = false;
102257
bool connecting = false;
103258

104-
// TODO: since we are single threaded, no need to buffer anything and we can directly use client->add with anything right in the body_send callback
259+
// TODO ref: https://github.com/xoseperez/espurna/pull/1909#issuecomment-533319480
260+
// since LWIP_NETIF_TX_SINGLE_PBUF is enabled, no need to buffer anything and we can directly use client->add with non-persistent data
105261
// buuut... this exposes asyncclient to the modules, maybe this needs a simple cbuf periodically flushing the data and this method simply filling it
106-
// (ref: AsyncTCPBuffer class in ESPAsyncTCP or ESPAsyncWebServer chuncked response callback)
107262
void trySend() {
108263
if (!client.canSend()) return;
109264
if (!on_body_send) {
@@ -113,6 +268,29 @@ class AsyncHttp {
113268
on_body_send(this, &client);
114269
}
115270

271+
bool trySendHeaders() {
272+
if (headers.done()) return true;
273+
274+
const auto& string = headers.current();
275+
const auto len = string.length();
276+
277+
if (!len) {
278+
return true;
279+
}
280+
281+
if (client.space() >= (len + 2)) {
282+
if (client.add(string.c_str(), len)) {
283+
if (!headers.next().length()) {
284+
client.add("\r\n", 2);
285+
}
286+
}
287+
client.send();
288+
}
289+
290+
return false;
291+
}
292+
293+
116294
protected:
117295

118296
static AsyncHttpError _timeoutError(AsyncHttpError::error_t error, const __FlashStringHelper* message, uint32_t ts) {
@@ -126,7 +304,7 @@ class AsyncHttp {
126304

127305
static void _onDisconnect(void* http_ptr, AsyncClient*) {
128306
AsyncHttp* http = static_cast<AsyncHttp*>(http_ptr);
129-
if (http->on_disconnected) http->on_disconnected(http);
307+
if (http->on_disconnected) http->on_disconnected(http);
130308
http->ts = 0;
131309
http->connected = false;
132310
http->connecting = false;
@@ -244,50 +422,41 @@ class AsyncHttp {
244422

245423
if (http->on_connected) http->on_connected(http);
246424

247-
const int headers_len =
248-
strlen_P(HTTP_REQUEST_TEMPLATE)
249-
+ http->method.length()
250-
+ http->host.length()
251-
+ http->path.length()
252-
+ 32;
253-
254-
int data_len = 0;
255-
if (http->cfg & HTTP_SEND) {
256-
if (!http->on_body_send) {
257-
ASYNC_HTTP_DEBUG("err | no send_body callback set\n");
258-
client->close(true);
259-
return;
425+
{
426+
size_t data_len = 0;
427+
if (http->cfg & HTTP_SEND) {
428+
if (!http->on_body_send) {
429+
ASYNC_HTTP_DEBUG("err | no send_body callback set\n");
430+
client->close(true);
431+
return;
432+
}
433+
// XXX: ...class instead of this multi-function?
434+
data_len = http->on_body_send(http, nullptr);
435+
char data_buf[22];
436+
snprintf(data_buf, sizeof(data_buf), "%u", data_len);
437+
http->headers.add({Headers::CONTENT_LENGTH, data_buf});
260438
}
261-
// XXX: ...class instead of this multi-function?
262-
data_len = http->on_body_send(http, nullptr);
263439
}
264440

265-
char* headers = (char *) malloc(headers_len + 1);
441+
{
442+
char buf[DEFAULT_PATH_BUFSIZE] = {0};
443+
int res = snprintf_P(
444+
buf, sizeof(buf), PSTR("%s %s HTTP/1.1\r\n"),
445+
http->method.c_str(), http->path.c_str()
446+
);
266447

267-
if (!headers) {
268-
ASYNC_HTTP_DEBUG("err | alloc %u fail\n", headers_len + 1);
269-
client->close(true);
270-
return;
271-
}
448+
if ((res < 0) || (static_cast<size_t>(res) > sizeof(buf))) {
449+
ASYNC_HTTP_DEBUG("err | could not print initial line\n");
450+
client->close(true);
451+
return;
452+
}
272453

273-
int res = snprintf_P(headers, headers_len + 1,
274-
HTTP_REQUEST_TEMPLATE,
275-
http->method.c_str(),
276-
http->path.c_str(),
277-
http->host.c_str(),
278-
data_len
279-
);
280-
if (res >= (headers_len + 1)) {
281-
ASYNC_HTTP_DEBUG("err | res>=len :: %u>=%u\n", res, headers_len + 1);
282-
free(headers);
283-
client->close(true);
284-
return;
454+
client->add(buf, res);
285455
}
286456

287-
client->write(headers);
288-
free(headers);
289-
290-
if (http->cfg & HTTP_SEND) http->trySend();
457+
if (http->trySendHeaders()) {
458+
if (http->cfg & HTTP_SEND) http->trySend();
459+
}
291460
}
292461

293462
static void _onError(void* http_ptr, AsyncClient* client, err_t err) {
@@ -298,11 +467,15 @@ class AsyncHttp {
298467
static void _onAck(void* http_ptr, AsyncClient* client, size_t, uint32_t) {
299468
AsyncHttp* http = static_cast<AsyncHttp*>(http_ptr);
300469
http->ts = millis();
301-
if (http->cfg & HTTP_SEND) http->trySend();
470+
if (http->trySendHeaders()) {
471+
if (http->cfg & HTTP_SEND) http->trySend();
472+
}
302473
}
303474

475+
304476
public:
305-
AsyncHttp() {
477+
AsyncHttp()
478+
{
306479
client.onDisconnect(_onDisconnect, this);
307480
client.onTimeout(_onTimeout, this);
308481
client.onPoll(_onPoll, this);
@@ -326,22 +499,33 @@ class AsyncHttp {
326499
this->ts = millis();
327500

328501
// Treat every method as GET (receive-only), exception for POST / PUT to send data out
502+
size_t headers_size = 3;
329503
this->cfg = HTTP_RECV;
330504
if (this->method.equals("POST") || this->method.equals("PUT")) {
331505
if (!this->on_body_send) return false;
332506
this->cfg = HTTP_SEND | HTTP_RECV;
507+
headers_size += 2;
333508
}
334509

335-
bool status = false;
510+
headers.reserve(headers_size);
511+
headers.clear();
512+
513+
headers.add({Headers::HOST, this->host.c_str()});
514+
headers.add({Headers::USER_AGENT, "ESPurna"});
515+
headers.add({Headers::CONNECTION, "close"});
516+
if (this->cfg & HTTP_SEND) {
517+
headers.add({Headers::CONTENT_TYPE, "application/x-www-form-urlencoded"});
518+
}
336519

520+
bool status = false;
337521
#if ASYNC_TCP_SSL_ENABLED
338522
status = client.connect(this->host.c_str(), this->port, use_ssl);
339523
#else
340524
status = client.connect(this->host.c_str(), this->port);
341525
#endif
342526

343527
this->connecting = status;
344-
528+
345529
if (!status) {
346530
client.close(true);
347531
}

0 commit comments

Comments
 (0)