@alteriom/painlessmesh 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/CONTRIBUTING.md +66 -8
- package/README.md +4 -2
- package/RELEASE_GUIDE.md +11 -1
- package/examples/sendToInternet/README.md +56 -4
- package/examples/sendToInternet/callmebot.h +139 -0
- package/examples/sendToInternet/sendToInternet.ino +166 -53
- package/library.json +1 -1
- package/library.properties +1 -1
- package/package.json +1 -1
- package/src/AlteriomPainlessMesh.h +3 -3
- package/src/arduino/wifi.hpp +179 -86
- package/src/painlessMeshSTA.cpp +38 -4
- package/src/painlessMeshSTA.h +13 -1
- package/src/painlessmesh/buffer.hpp +11 -6
- package/src/painlessmesh/callback.hpp +7 -0
- package/src/painlessmesh/gateway.hpp +649 -27
- package/src/painlessmesh/layout.hpp +16 -0
- package/src/painlessmesh/logger.hpp +14 -1
- package/src/painlessmesh/mesh.hpp +410 -103
- package/src/painlessmesh/router.hpp +17 -7
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
#endif
|
|
40
40
|
#include "painlessmesh/plugin.hpp"
|
|
41
41
|
#include "painlessmesh/protocol.hpp"
|
|
42
|
+
#include "painlessmesh/validation.hpp"
|
|
42
43
|
|
|
43
44
|
#include <functional>
|
|
44
45
|
#include <map>
|
|
@@ -568,6 +569,8 @@ class InternetHealthChecker {
|
|
|
568
569
|
* @return true if connection succeeded
|
|
569
570
|
*/
|
|
570
571
|
bool checkNow() {
|
|
572
|
+
// A full probe answers the question the on-demand budget exists for.
|
|
573
|
+
onDemandSpent_ = false;
|
|
571
574
|
status_.checkCount++;
|
|
572
575
|
status_.lastCheckTime = millis();
|
|
573
576
|
|
|
@@ -592,6 +595,27 @@ class InternetHealthChecker {
|
|
|
592
595
|
return connected;
|
|
593
596
|
}
|
|
594
597
|
|
|
598
|
+
/**
|
|
599
|
+
* @brief Probe once outside the schedule, for a caller that needs the
|
|
600
|
+
* answer now (issue #450)
|
|
601
|
+
*
|
|
602
|
+
* A bridge's first periodic probe runs while its station is still
|
|
603
|
+
* associating and fails, and the next is a full interval away. A send in
|
|
604
|
+
* that window may spend one extra probe to learn that the uplink has come
|
|
605
|
+
* up since. One: the probe is a blocking connect with a timeout, so the
|
|
606
|
+
* budget is a single on-demand probe per periodic one. A node whose uplink
|
|
607
|
+
* really is down pays it once per interval, not on every call.
|
|
608
|
+
*
|
|
609
|
+
* @return true if Internet is reachable now
|
|
610
|
+
*/
|
|
611
|
+
bool checkOnDemand() {
|
|
612
|
+
if (status_.available) return true;
|
|
613
|
+
if (onDemandSpent_) return false;
|
|
614
|
+
const bool connected = checkNow();
|
|
615
|
+
onDemandSpent_ = true;
|
|
616
|
+
return connected;
|
|
617
|
+
}
|
|
618
|
+
|
|
595
619
|
/**
|
|
596
620
|
* @brief Get check interval
|
|
597
621
|
* @return Check interval in milliseconds
|
|
@@ -667,6 +691,13 @@ class InternetHealthChecker {
|
|
|
667
691
|
status_.lastError = "Mock: No Internet in test environment";
|
|
668
692
|
return false;
|
|
669
693
|
#elif defined(ESP32) || defined(ESP8266)
|
|
694
|
+
// No station, no uplink: answer at once instead of spending the connect
|
|
695
|
+
// timeout on a link that does not exist. This is what keeps the
|
|
696
|
+
// on-demand probe cheap on a bridge whose router is down.
|
|
697
|
+
if (WiFi.status() != WL_CONNECTED) {
|
|
698
|
+
status_.lastError = "Station not connected";
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
670
701
|
WiFiClient client;
|
|
671
702
|
uint32_t started = millis();
|
|
672
703
|
#ifdef ESP32
|
|
@@ -704,6 +735,7 @@ class InternetHealthChecker {
|
|
|
704
735
|
// State
|
|
705
736
|
InternetStatus status_;
|
|
706
737
|
InternetChangedCallback_t connectivityChangedCallback_;
|
|
738
|
+
bool onDemandSpent_ = false;
|
|
707
739
|
|
|
708
740
|
#ifdef PAINLESSMESH_BOOST
|
|
709
741
|
bool mockConnected_ = false;
|
|
@@ -853,13 +885,26 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
853
885
|
*/
|
|
854
886
|
bool requiresAck = false;
|
|
855
887
|
|
|
888
|
+
/**
|
|
889
|
+
* @brief Random value drawn once per sendToInternet() call
|
|
890
|
+
*
|
|
891
|
+
* The same on every attempt at that call, and part of its X-Request-Id and
|
|
892
|
+
* Idempotency-Key (requestIdFor()). messageId alone repeats: its counter is
|
|
893
|
+
* 16 bits, so a node that sends more than 65,535 requests in one boot
|
|
894
|
+
* reissues old ids, and a service that remembers keys would drop the new
|
|
895
|
+
* request as a repeat. 0 from a node that predates the field. JSON key
|
|
896
|
+
* "nonce", omitted when 0.
|
|
897
|
+
*/
|
|
898
|
+
uint32_t requestNonce = 0;
|
|
899
|
+
|
|
856
900
|
/**
|
|
857
901
|
* @brief Number of additional JSON fields in this package
|
|
858
902
|
*
|
|
859
903
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
860
|
-
* Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack
|
|
904
|
+
* Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack,
|
|
905
|
+
* nonce = 10 fields
|
|
861
906
|
*/
|
|
862
|
-
static constexpr int numPackageFields =
|
|
907
|
+
static constexpr int numPackageFields = 10;
|
|
863
908
|
|
|
864
909
|
/**
|
|
865
910
|
* @brief Default constructor
|
|
@@ -883,6 +928,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
883
928
|
priority = jsonObj["prio"];
|
|
884
929
|
retryCount = jsonObj["retry"];
|
|
885
930
|
requiresAck = jsonObj["ack"] | false;
|
|
931
|
+
requestNonce = jsonObj["nonce"] | 0UL;
|
|
886
932
|
|
|
887
933
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
888
934
|
if (jsonObj.containsKey("dest_url"))
|
|
@@ -920,6 +966,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
920
966
|
jsonObj["content"] = contentType;
|
|
921
967
|
jsonObj["retry"] = retryCount;
|
|
922
968
|
jsonObj["ack"] = requiresAck;
|
|
969
|
+
if (requestNonce != 0) jsonObj["nonce"] = requestNonce;
|
|
923
970
|
return jsonObj;
|
|
924
971
|
}
|
|
925
972
|
|
|
@@ -957,30 +1004,89 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
957
1004
|
* @return A unique message ID
|
|
958
1005
|
*/
|
|
959
1006
|
static uint32_t generateMessageId(uint32_t nodeId) {
|
|
960
|
-
|
|
1007
|
+
// The counter starts at a random point each boot. Starting at zero, the
|
|
1008
|
+
// first request after every reboot carried the same id as the first
|
|
1009
|
+
// request of the boot before -- and so the same X-Request-Id and
|
|
1010
|
+
// Idempotency-Key, which a service that remembers keys drops as a repeat.
|
|
1011
|
+
static uint16_t counter =
|
|
1012
|
+
static_cast<uint16_t>(validation::SecureRandom::generate());
|
|
961
1013
|
++counter;
|
|
1014
|
+
if (counter == 0) ++counter;
|
|
962
1015
|
// Combine node ID (upper 16 bits) with counter (lower 16 bits)
|
|
963
1016
|
return ((nodeId & 0xFFFF) << 16) | counter;
|
|
964
1017
|
}
|
|
965
1018
|
|
|
966
1019
|
};
|
|
967
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* @brief HTTPClient transport errors, by what they say about the request
|
|
1023
|
+
*
|
|
1024
|
+
* HTTPClient::GET()/POST() return a negative code when the request failed
|
|
1025
|
+
* below HTTP. The ESP32 and ESP8266 cores number them the same way. What
|
|
1026
|
+
* matters to a retry is whether the request can have reached the server:
|
|
1027
|
+
* resending one that did delivers it twice, which for a request with an
|
|
1028
|
+
* effect -- a message, a payment, a counter -- is a second effect (the rig
|
|
1029
|
+
* showed a timed-out send issued four times).
|
|
1030
|
+
*
|
|
1031
|
+
* Where each is raised, in both cores' HTTPClient::sendRequest() for the
|
|
1032
|
+
* buffer GET()/POST() the gateway uses: -1, -2 and -3 before the request is
|
|
1033
|
+
* complete; everything else from handleHeaderResponse(), which runs after the
|
|
1034
|
+
* whole request was written -- so -4 is a connection that closed before the
|
|
1035
|
+
* reply began and -7 a reply that was not HTTP, both with the request already
|
|
1036
|
+
* at the server. -6 and -8 come from stream sends and from reading a body.
|
|
1037
|
+
*/
|
|
1038
|
+
enum HttpTransportError : int {
|
|
1039
|
+
HTTP_TRANSPORT_CONNECTION_REFUSED = -1, ///< no connection was made
|
|
1040
|
+
HTTP_TRANSPORT_SEND_HEADER_FAILED = -2, ///< the request line never completed
|
|
1041
|
+
HTTP_TRANSPORT_SEND_PAYLOAD_FAILED = -3, ///< the body never completed
|
|
1042
|
+
HTTP_TRANSPORT_NOT_CONNECTED = -4, ///< closed before the reply began; sent
|
|
1043
|
+
HTTP_TRANSPORT_CONNECTION_LOST = -5, ///< dropped while reading the reply; sent
|
|
1044
|
+
HTTP_TRANSPORT_NO_STREAM = -6, ///< no stream to send or read with
|
|
1045
|
+
HTTP_TRANSPORT_NO_HTTP_SERVER = -7, ///< the reply was not HTTP; sent
|
|
1046
|
+
HTTP_TRANSPORT_TOO_LESS_RAM = -8, ///< out of memory sending or reading
|
|
1047
|
+
HTTP_TRANSPORT_ENCODING = -9, ///< the reply arrived malformed; sent
|
|
1048
|
+
HTTP_TRANSPORT_STREAM_WRITE = -10, ///< the reply could not be stored; sent
|
|
1049
|
+
HTTP_TRANSPORT_READ_TIMEOUT = -11, ///< sent, and no reply in time
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* @brief Can a request that failed with this transport error have reached
|
|
1054
|
+
* the server?
|
|
1055
|
+
*
|
|
1056
|
+
* True -- the safe answer -- for every code that is not known to fail before
|
|
1057
|
+
* the request was complete, including codes this library does not know.
|
|
1058
|
+
*/
|
|
1059
|
+
inline bool transportErrorMayHaveReachedServer(int rawCode) {
|
|
1060
|
+
switch (rawCode) {
|
|
1061
|
+
case HTTP_TRANSPORT_CONNECTION_REFUSED:
|
|
1062
|
+
case HTTP_TRANSPORT_SEND_HEADER_FAILED:
|
|
1063
|
+
case HTTP_TRANSPORT_SEND_PAYLOAD_FAILED:
|
|
1064
|
+
return false;
|
|
1065
|
+
default:
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
968
1070
|
/**
|
|
969
1071
|
* @brief The outcome of a gateway HTTP request, classified for the ack
|
|
970
1072
|
*
|
|
971
1073
|
* HTTPClient::GET()/POST() return an int with two distinct meanings: a
|
|
972
1074
|
* positive value is an HTTP status code from the destination, while a
|
|
973
|
-
* negative value is one of the client's own transport errors (
|
|
974
|
-
*
|
|
975
|
-
* produced by either.
|
|
1075
|
+
* negative value is one of the client's own transport errors (see
|
|
1076
|
+
* HttpTransportError). Zero is not produced by either.
|
|
976
1077
|
*
|
|
977
1078
|
* GatewayAckPackage::httpStatus is a uint16_t, so a negative code cannot be
|
|
978
|
-
* forwarded as-is. Transport errors are reported as httpStatus 0
|
|
979
|
-
*
|
|
980
|
-
* the
|
|
1079
|
+
* forwarded as-is. Transport errors are reported as httpStatus 0 with the
|
|
1080
|
+
* cause in GatewayAckPackage::error, and whether the origin node may resend
|
|
1081
|
+
* the request travels in GatewayAckPackage::retryable.
|
|
1082
|
+
*
|
|
1083
|
+
* The library applies HTTP's meaning of a status and nothing else. Whether a
|
|
1084
|
+
* particular service's reply means what the application wanted -- a message
|
|
1085
|
+
* really queued, a record really written -- is the application's decision,
|
|
1086
|
+
* made from the status and the response the result carries.
|
|
981
1087
|
*/
|
|
982
1088
|
struct HttpRequestOutcome {
|
|
983
|
-
/** True only for
|
|
1089
|
+
/** True only for 200, 201, 202 and 204. */
|
|
984
1090
|
bool success = false;
|
|
985
1091
|
|
|
986
1092
|
/** Value to place in GatewayAckPackage::httpStatus (0 for transport errors). */
|
|
@@ -988,31 +1094,503 @@ struct HttpRequestOutcome {
|
|
|
988
1094
|
|
|
989
1095
|
/** True when the client failed before any HTTP status was received. */
|
|
990
1096
|
bool transportError = false;
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* True for a 2xx outside 200, 201, 202 and 204. 203 says a proxy
|
|
1100
|
+
* transformed the reply; 205, 206 and 208 are not what a request to an API
|
|
1101
|
+
* endpoint expects. The server answered, so it is never retried; it is
|
|
1102
|
+
* reported as a failure the application can inspect.
|
|
1103
|
+
*/
|
|
1104
|
+
bool unverifiedStatus = false;
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Whether resending the identical request is safe and useful: the request
|
|
1108
|
+
* cannot have reached the server, or the server said it did not take it
|
|
1109
|
+
* and to come back (429 Too Many Requests, 503 Service Unavailable). A
|
|
1110
|
+
* reply that may mean the request was processed -- any 2xx, a 500, a
|
|
1111
|
+
* gateway timeout, a read timeout -- is not retried, because a retry would
|
|
1112
|
+
* be a second copy of it.
|
|
1113
|
+
*/
|
|
1114
|
+
bool retryable = false;
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* One-line summary of the response body, for the error an application
|
|
1118
|
+
* reads; empty on success and when no body was read.
|
|
1119
|
+
*/
|
|
1120
|
+
TSTRING reason;
|
|
991
1121
|
};
|
|
992
1122
|
|
|
993
1123
|
/**
|
|
994
|
-
*
|
|
1124
|
+
* Longest response-body summary carried in an acknowledgment. Long enough
|
|
1125
|
+
* for a service that repeats the request back before its verdict -- the
|
|
1126
|
+
* reply in issue #463 spent 97 of 120 characters on the echo and was cut
|
|
1127
|
+
* before the part that said why.
|
|
1128
|
+
*/
|
|
1129
|
+
static const size_t GATEWAY_RESPONSE_REASON_MAX = 240;
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* How much of a response body the gateway keeps for the summary: its first
|
|
1133
|
+
* GATEWAY_RESPONSE_HEAD_BYTES and its last GATEWAY_RESPONSE_TAIL_BYTES.
|
|
1134
|
+
* Enough for a service's verdict at either end, small enough that an HTML
|
|
1135
|
+
* error page cannot eat an ESP8266's heap.
|
|
1136
|
+
*/
|
|
1137
|
+
static const size_t GATEWAY_RESPONSE_HEAD_BYTES = 512;
|
|
1138
|
+
static const size_t GATEWAY_RESPONSE_TAIL_BYTES = 256;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* How far into a body the gateway reads looking for its end. Past this the
|
|
1142
|
+
* tail kept is the last of what was read, not the body's end; reading on
|
|
1143
|
+
* would hold the uplink for a reply nobody summarises.
|
|
1144
|
+
*/
|
|
1145
|
+
static const size_t GATEWAY_RESPONSE_SCAN_BYTES = 8192;
|
|
1146
|
+
|
|
1147
|
+
/** How long the gateway reads the body for, after the status. */
|
|
1148
|
+
static const uint32_t GATEWAY_RESPONSE_HEAD_TIMEOUT_MS = 250;
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Longest wait a server's Retry-After may impose before a retry. A longer one
|
|
1152
|
+
* is not honoured with a retry at all: the request fails and says when the
|
|
1153
|
+
* server asked to be retried, which is the application's call to make.
|
|
1154
|
+
*/
|
|
1155
|
+
static const uint32_t GATEWAY_RETRY_AFTER_MAX_MS = 60000;
|
|
1156
|
+
|
|
1157
|
+
inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
1158
|
+
#if defined(PAINLESSMESH_BOOST)
|
|
1159
|
+
return haystack.find(needle) != std::string::npos;
|
|
1160
|
+
#else
|
|
1161
|
+
return haystack.indexOf(needle) >= 0;
|
|
1162
|
+
#endif
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* @brief Reduce a response body to one line fit for a log or an error string
|
|
995
1167
|
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
1168
|
+
* Tags are dropped and whitespace collapsed, so an HTML page reads as text in
|
|
1169
|
+
* a serial log. A body longer than maxLen keeps its beginning and its end,
|
|
1170
|
+
* joined by " ... ": services often lead with an echo of the request and
|
|
1171
|
+
* finish with the verdict, and a summary that keeps only the beginning keeps
|
|
1172
|
+
* the echo and loses the verdict (issue #463).
|
|
1173
|
+
*/
|
|
1174
|
+
inline TSTRING summarizeResponseBody(
|
|
1175
|
+
const TSTRING& body, size_t maxLen = GATEWAY_RESPONSE_REASON_MAX) {
|
|
1176
|
+
TSTRING text;
|
|
1177
|
+
bool inTag = false;
|
|
1178
|
+
bool pendingSpace = false;
|
|
1179
|
+
for (size_t i = 0; i < body.length(); ++i) {
|
|
1180
|
+
const char c = body[i];
|
|
1181
|
+
if (c == '<') {
|
|
1182
|
+
inTag = true;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (c == '>') {
|
|
1186
|
+
inTag = false;
|
|
1187
|
+
pendingSpace = true;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (inTag) continue;
|
|
1191
|
+
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
|
1192
|
+
pendingSpace = true;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (pendingSpace && text.length() > 0) text += ' ';
|
|
1196
|
+
pendingSpace = false;
|
|
1197
|
+
text += c;
|
|
1198
|
+
}
|
|
1199
|
+
if (text.length() <= maxLen) return text;
|
|
1200
|
+
|
|
1201
|
+
static const char JOIN[] = " ... ";
|
|
1202
|
+
const size_t joinLength = sizeof(JOIN) - 1;
|
|
1203
|
+
const size_t room = maxLen > joinLength ? maxLen - joinLength : 0;
|
|
1204
|
+
const size_t head = room / 2;
|
|
1205
|
+
const size_t tail = room - head;
|
|
1206
|
+
TSTRING out;
|
|
1207
|
+
for (size_t i = 0; i < head; ++i) out += text[i];
|
|
1208
|
+
out += JOIN;
|
|
1209
|
+
for (size_t i = text.length() - tail; i < text.length(); ++i) out += text[i];
|
|
1210
|
+
return out;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* @brief The start and the end of a response body, read a byte at a time
|
|
1215
|
+
*
|
|
1216
|
+
* The gateway cannot keep a whole body, and a service may put its verdict at
|
|
1217
|
+
* either end: CallMeBot echoes the request first and says why last (#463), so
|
|
1218
|
+
* a reply of a kilobyte kept as its first 512 bytes had lost the part that
|
|
1219
|
+
* mattered before any summary ran. This keeps the first
|
|
1220
|
+
* GATEWAY_RESPONSE_HEAD_BYTES and a rolling last GATEWAY_RESPONSE_TAIL_BYTES,
|
|
1221
|
+
* and text() joins them with " ... " when bytes were dropped between.
|
|
1222
|
+
*/
|
|
1223
|
+
class ResponseExcerpt {
|
|
1224
|
+
public:
|
|
1225
|
+
/** Keep one more byte; false once GATEWAY_RESPONSE_SCAN_BYTES were seen. */
|
|
1226
|
+
bool add(char c) {
|
|
1227
|
+
++seen_;
|
|
1228
|
+
if (head_.length() < GATEWAY_RESPONSE_HEAD_BYTES) {
|
|
1229
|
+
head_ += c;
|
|
1230
|
+
} else {
|
|
1231
|
+
tail_[(tailStart_ + tailLength_) % GATEWAY_RESPONSE_TAIL_BYTES] = c;
|
|
1232
|
+
if (tailLength_ < GATEWAY_RESPONSE_TAIL_BYTES) {
|
|
1233
|
+
++tailLength_;
|
|
1234
|
+
} else {
|
|
1235
|
+
tailStart_ = (tailStart_ + 1) % GATEWAY_RESPONSE_TAIL_BYTES;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return seen_ < GATEWAY_RESPONSE_SCAN_BYTES;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
void add(const TSTRING& text) {
|
|
1242
|
+
for (size_t i = 0; i < text.length(); ++i) {
|
|
1243
|
+
if (!add(text[i])) return;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
TSTRING text() const {
|
|
1248
|
+
TSTRING out = head_;
|
|
1249
|
+
if (seen_ > head_.length() + tailLength_) out += " ... ";
|
|
1250
|
+
for (size_t i = 0; i < tailLength_; ++i) {
|
|
1251
|
+
out += tail_[(tailStart_ + i) % GATEWAY_RESPONSE_TAIL_BYTES];
|
|
1252
|
+
}
|
|
1253
|
+
return out;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
private:
|
|
1257
|
+
TSTRING head_;
|
|
1258
|
+
char tail_[GATEWAY_RESPONSE_TAIL_BYTES] = {};
|
|
1259
|
+
size_t tailStart_ = 0;
|
|
1260
|
+
size_t tailLength_ = 0;
|
|
1261
|
+
size_t seen_ = 0;
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* @brief Removes HTTP/1.1 chunked transfer framing from a body read a byte at
|
|
1266
|
+
* a time, and passes the content on to a ResponseExcerpt
|
|
1267
|
+
*
|
|
1268
|
+
* HTTPClient's getString() decodes a chunked body but keeps all of it; the
|
|
1269
|
+
* gateway reads the raw stream to keep only a bounded excerpt, and the raw
|
|
1270
|
+
* stream is the framing: CallMeBot's real reply reached the application as
|
|
1271
|
+
* "a6 Message to: ... Message queued. ... 0" (hardware rig, 2026-09-15) --
|
|
1272
|
+
* the chunk size in front, the terminating zero-length chunk behind. This
|
|
1273
|
+
* reads the framing (hex size, optional ;extensions, CRLF, data, CRLF, ...,
|
|
1274
|
+
* 0, trailers) and forwards only the data. A malformed size line stops
|
|
1275
|
+
* decoding rather than guessing: what was decoded so far is kept.
|
|
1276
|
+
*/
|
|
1277
|
+
class ChunkedBodyDecoder {
|
|
1278
|
+
public:
|
|
1279
|
+
explicit ChunkedBodyDecoder(ResponseExcerpt& excerpt) : excerpt_(excerpt) {}
|
|
1280
|
+
|
|
1281
|
+
/** One byte of the raw body; false once the body ended or the excerpt is
|
|
1282
|
+
* full, when the caller can stop reading. */
|
|
1283
|
+
bool add(char c) {
|
|
1284
|
+
switch (state_) {
|
|
1285
|
+
case State::Size:
|
|
1286
|
+
if (c == '\r') {
|
|
1287
|
+
state_ = State::SizeLf;
|
|
1288
|
+
} else if (c == ';') {
|
|
1289
|
+
state_ = State::Extension;
|
|
1290
|
+
} else if (c == ' ' || c == '\t') {
|
|
1291
|
+
// Tolerated around the size, as many servers emit it.
|
|
1292
|
+
} else {
|
|
1293
|
+
const int digit = hexValue(c);
|
|
1294
|
+
if (digit < 0 || sizeDigits_ >= 8) return stop();
|
|
1295
|
+
remaining_ = (remaining_ << 4) | static_cast<uint32_t>(digit);
|
|
1296
|
+
++sizeDigits_;
|
|
1297
|
+
}
|
|
1298
|
+
return true;
|
|
1299
|
+
case State::Extension:
|
|
1300
|
+
if (c == '\r') state_ = State::SizeLf;
|
|
1301
|
+
return true;
|
|
1302
|
+
case State::SizeLf:
|
|
1303
|
+
if (c != '\n' || sizeDigits_ == 0) return stop();
|
|
1304
|
+
sizeDigits_ = 0;
|
|
1305
|
+
if (remaining_ == 0) {
|
|
1306
|
+
state_ = State::Done;
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
state_ = State::Data;
|
|
1310
|
+
return true;
|
|
1311
|
+
case State::Data:
|
|
1312
|
+
--remaining_;
|
|
1313
|
+
if (remaining_ == 0) state_ = State::DataCr;
|
|
1314
|
+
if (!excerpt_.add(c)) {
|
|
1315
|
+
state_ = State::Done;
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
return true;
|
|
1319
|
+
case State::DataCr:
|
|
1320
|
+
if (c != '\r') return stop();
|
|
1321
|
+
state_ = State::DataLf;
|
|
1322
|
+
return true;
|
|
1323
|
+
case State::DataLf:
|
|
1324
|
+
if (c != '\n') return stop();
|
|
1325
|
+
state_ = State::Size;
|
|
1326
|
+
return true;
|
|
1327
|
+
case State::Done:
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
void add(const TSTRING& raw) {
|
|
1334
|
+
for (size_t i = 0; i < raw.length(); ++i) {
|
|
1335
|
+
if (!add(raw[i])) return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** True when the terminating zero-length chunk was read. */
|
|
1340
|
+
bool complete() const { return state_ == State::Done && !malformed_; }
|
|
1341
|
+
|
|
1342
|
+
private:
|
|
1343
|
+
enum class State { Size, Extension, SizeLf, Data, DataCr, DataLf, Done };
|
|
1344
|
+
|
|
1345
|
+
static int hexValue(char c) {
|
|
1346
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
1347
|
+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
1348
|
+
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
1349
|
+
return -1;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
bool stop() {
|
|
1353
|
+
malformed_ = true;
|
|
1354
|
+
state_ = State::Done;
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
ResponseExcerpt& excerpt_;
|
|
1359
|
+
State state_ = State::Size;
|
|
1360
|
+
uint32_t remaining_ = 0;
|
|
1361
|
+
uint8_t sizeDigits_ = 0;
|
|
1362
|
+
bool malformed_ = false;
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
/** Whether a Transfer-Encoding header value names chunked encoding. */
|
|
1366
|
+
inline bool transferEncodingIsChunked(const TSTRING& value) {
|
|
1367
|
+
TSTRING lower;
|
|
1368
|
+
for (size_t i = 0; i < value.length(); ++i) {
|
|
1369
|
+
const char c = value[i];
|
|
1370
|
+
lower += (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
|
1371
|
+
}
|
|
1372
|
+
return responseContains(lower, "chunked");
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* @brief The delay a Retry-After header asks for, in milliseconds
|
|
1377
|
+
*
|
|
1378
|
+
* Only the delay-seconds form is read; an HTTP-date, or anything else, is
|
|
1379
|
+
* 0 -- no instruction -- rather than a guess. Values past
|
|
1380
|
+
* GATEWAY_RETRY_AFTER_MAX_MS are returned as they are, so the caller can see
|
|
1381
|
+
* the server asked for longer than it will wait.
|
|
1382
|
+
*/
|
|
1383
|
+
inline uint32_t parseRetryAfterMs(const TSTRING& value) {
|
|
1384
|
+
uint64_t seconds = 0;
|
|
1385
|
+
size_t digits = 0;
|
|
1386
|
+
for (size_t i = 0; i < value.length(); ++i) {
|
|
1387
|
+
const char c = value[i];
|
|
1388
|
+
if (c == ' ' || c == '\t') {
|
|
1389
|
+
if (digits == 0) continue;
|
|
1390
|
+
break;
|
|
1391
|
+
}
|
|
1392
|
+
if (c < '0' || c > '9') return 0;
|
|
1393
|
+
seconds = seconds * 10 + static_cast<uint64_t>(c - '0');
|
|
1394
|
+
if (seconds > 0xFFFFFFFFULL / 1000ULL) return 0xFFFFFFFFUL;
|
|
1395
|
+
++digits;
|
|
1396
|
+
}
|
|
1397
|
+
return digits == 0 ? 0 : static_cast<uint32_t>(seconds * 1000ULL);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* @brief The identifier the gateway sends with a request, as both
|
|
1402
|
+
* X-Request-Id and Idempotency-Key
|
|
1403
|
+
*
|
|
1404
|
+
* The same for every attempt at one sendToInternet() call, so a service that
|
|
1405
|
+
* honours Idempotency-Key treats a retry as the request it already has, and
|
|
1406
|
+
* anything recording requests can count the copies one call produced.
|
|
1407
|
+
*
|
|
1408
|
+
* The request's nonce keeps it unique beyond its messageId, whose counter
|
|
1409
|
+
* wraps after 65,535 requests in a boot. A package from a node that predates
|
|
1410
|
+
* the nonce (0) keeps the two-part form it always had.
|
|
1411
|
+
*/
|
|
1412
|
+
inline TSTRING requestIdFor(uint32_t originNode, uint32_t messageId,
|
|
1413
|
+
uint32_t requestNonce = 0) {
|
|
1414
|
+
char buffer[40];
|
|
1415
|
+
if (requestNonce == 0) {
|
|
1416
|
+
snprintf(buffer, sizeof(buffer), "pm-%08x-%08x",
|
|
1417
|
+
static_cast<unsigned>(originNode), static_cast<unsigned>(messageId));
|
|
1418
|
+
} else {
|
|
1419
|
+
snprintf(buffer, sizeof(buffer), "pm-%08x-%08x-%08x",
|
|
1420
|
+
static_cast<unsigned>(originNode), static_cast<unsigned>(messageId),
|
|
1421
|
+
static_cast<unsigned>(requestNonce));
|
|
1422
|
+
}
|
|
1423
|
+
return TSTRING(buffer);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/** A request nonce: random, and never the 0 that means "none". */
|
|
1427
|
+
inline uint32_t newRequestNonce() {
|
|
1428
|
+
uint32_t nonce = validation::SecureRandom::generate();
|
|
1429
|
+
return nonce != 0 ? nonce : 1;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* @brief Classify an HTTPClient result for the gateway acknowledgment
|
|
1434
|
+
*
|
|
1435
|
+
* HTTP semantics only. 200, 201, 202 and 204 are a success. Any other 2xx is
|
|
1436
|
+
* an unverified failure the server answered; 1xx, 3xx, 4xx and 5xx are
|
|
1437
|
+
* failures. A retry is allowed only when it cannot produce a second copy of
|
|
1438
|
+
* the request: a transport error that failed before the request was complete,
|
|
1439
|
+
* or 429/503, where the server says it did not take the request. The reason
|
|
1440
|
+
* is a summary of whatever body was read, so the application learns why.
|
|
999
1441
|
*
|
|
1000
1442
|
* @param rawCode The int returned by HTTPClient::GET() or ::POST()
|
|
1443
|
+
* @param body The start of the response body, empty if none was read
|
|
1001
1444
|
* @return Classified outcome, safe to place in a GatewayAckPackage
|
|
1002
1445
|
*/
|
|
1003
|
-
inline HttpRequestOutcome classifyHttpResult(int rawCode
|
|
1446
|
+
inline HttpRequestOutcome classifyHttpResult(int rawCode,
|
|
1447
|
+
const TSTRING& body = TSTRING()) {
|
|
1004
1448
|
HttpRequestOutcome outcome;
|
|
1005
1449
|
if (rawCode <= 0) {
|
|
1006
1450
|
outcome.transportError = true;
|
|
1451
|
+
outcome.retryable = !transportErrorMayHaveReachedServer(rawCode);
|
|
1007
1452
|
return outcome;
|
|
1008
1453
|
}
|
|
1009
1454
|
outcome.ackStatus =
|
|
1010
1455
|
static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
|
|
1011
|
-
outcome.success =
|
|
1012
|
-
|
|
1456
|
+
outcome.success =
|
|
1457
|
+
rawCode == 200 || rawCode == 201 || rawCode == 202 || rawCode == 204;
|
|
1458
|
+
outcome.unverifiedStatus = !outcome.success && rawCode >= 200 && rawCode < 300;
|
|
1459
|
+
outcome.retryable = rawCode == 429 || rawCode == 503;
|
|
1460
|
+
if (!outcome.success) outcome.reason = summarizeResponseBody(body);
|
|
1013
1461
|
return outcome;
|
|
1014
1462
|
}
|
|
1015
1463
|
|
|
1464
|
+
/**
|
|
1465
|
+
* The phrase a node puts in a failed GATEWAY_ACK when it received a gateway
|
|
1466
|
+
* request but serves none: a bridge that rebooted, crashed or was reflashed
|
|
1467
|
+
* as a regular node announces nothing, and its peers keep routing Internet
|
|
1468
|
+
* requests to it until its last bridge status ages out. The origin node
|
|
1469
|
+
* recognises the phrase, forgets that node as a gateway, and retries through
|
|
1470
|
+
* the next one. Changing it breaks that recognition across a mixed fleet.
|
|
1471
|
+
*/
|
|
1472
|
+
static const char GATEWAY_NOT_A_GATEWAY_PHRASE[] = "is not an Internet gateway";
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* How long a destination whose name failed to resolve is refused without
|
|
1476
|
+
* another lookup (issue #453). On ESP32 a DNS lookup has no timeout this
|
|
1477
|
+
* library can set, so a dead name stalls the cooperative scheduler for the
|
|
1478
|
+
* resolver's own patience; once a minute is survivable, once per attempt --
|
|
1479
|
+
* with the origin node retrying and the bridge's own sends adding theirs --
|
|
1480
|
+
* was not. On ESP8266 the core bounds HTTPClient's own lookup by the HTTP
|
|
1481
|
+
* timeout, so the gateway performs no separate lookup there and this cache
|
|
1482
|
+
* is never fed.
|
|
1483
|
+
*/
|
|
1484
|
+
static const uint32_t GATEWAY_DNS_NEGATIVE_TTL_MS = 60000;
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* @brief The host of an http(s) URL, without scheme, port, path or query
|
|
1488
|
+
*
|
|
1489
|
+
* "https://api.example.com:8443/x?y" -> "api.example.com". Empty when the
|
|
1490
|
+
* URL has no host.
|
|
1491
|
+
*/
|
|
1492
|
+
inline TSTRING hostFromUrl(const TSTRING& url) {
|
|
1493
|
+
const char* s = url.c_str();
|
|
1494
|
+
const size_t n = url.length();
|
|
1495
|
+
size_t i = 0;
|
|
1496
|
+
for (size_t k = 0; k + 2 < n; ++k) {
|
|
1497
|
+
if (s[k] == ':' && s[k + 1] == '/' && s[k + 2] == '/') {
|
|
1498
|
+
i = k + 3;
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
size_t j = i;
|
|
1503
|
+
while (j < n && s[j] != '/' && s[j] != '?' && s[j] != '#' && s[j] != ':') ++j;
|
|
1504
|
+
TSTRING host;
|
|
1505
|
+
for (size_t k = i; k < j; ++k) host += s[k];
|
|
1506
|
+
return host;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/** True for a dotted-decimal IPv4 literal, which needs no DNS. */
|
|
1510
|
+
inline bool looksLikeIpLiteral(const TSTRING& host) {
|
|
1511
|
+
if (host.length() == 0) return false;
|
|
1512
|
+
for (size_t i = 0; i < host.length(); ++i) {
|
|
1513
|
+
const char c = host[i];
|
|
1514
|
+
if (!(c >= '0' && c <= '9') && c != '.') return false;
|
|
1515
|
+
}
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* @brief A few hosts that recently failed to resolve, and when
|
|
1521
|
+
*
|
|
1522
|
+
* Small and fixed: a gateway talks to a handful of destinations. A host is
|
|
1523
|
+
* remembered with a TTL; while it is within it, isFailing() says so and the
|
|
1524
|
+
* caller answers the request without a lookup.
|
|
1525
|
+
*/
|
|
1526
|
+
class NegativeDnsCache {
|
|
1527
|
+
public:
|
|
1528
|
+
// An enum, not a static const member: the test suite passes it to Catch2 by
|
|
1529
|
+
// reference, which needs a definition C++14 cannot give an in-class
|
|
1530
|
+
// constant without an out-of-line one.
|
|
1531
|
+
enum : size_t { SLOTS = 4 };
|
|
1532
|
+
|
|
1533
|
+
void remember(const TSTRING& host, uint32_t nowMs,
|
|
1534
|
+
uint32_t ttlMs = GATEWAY_DNS_NEGATIVE_TTL_MS) {
|
|
1535
|
+
Entry* slot = find(host);
|
|
1536
|
+
if (slot == nullptr) {
|
|
1537
|
+
slot = &entries_[0];
|
|
1538
|
+
for (size_t i = 0; i < SLOTS; ++i) {
|
|
1539
|
+
if (!entries_[i].used) {
|
|
1540
|
+
slot = &entries_[i];
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
if (static_cast<int32_t>(entries_[i].at - slot->at) < 0) slot = &entries_[i];
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
slot->used = true;
|
|
1547
|
+
slot->host = host;
|
|
1548
|
+
slot->at = nowMs;
|
|
1549
|
+
slot->ttl = ttlMs;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** Is `host` inside its negative TTL? `ageMs` receives how long ago it failed. */
|
|
1553
|
+
bool isFailing(const TSTRING& host, uint32_t nowMs, uint32_t* ageMs = nullptr) {
|
|
1554
|
+
Entry* slot = find(host);
|
|
1555
|
+
if (slot == nullptr) return false;
|
|
1556
|
+
const uint32_t age = nowMs - slot->at;
|
|
1557
|
+
if (age >= slot->ttl) {
|
|
1558
|
+
slot->used = false;
|
|
1559
|
+
return false;
|
|
1560
|
+
}
|
|
1561
|
+
if (ageMs != nullptr) *ageMs = age;
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
void forget(const TSTRING& host) {
|
|
1566
|
+
Entry* slot = find(host);
|
|
1567
|
+
if (slot != nullptr) slot->used = false;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
size_t size() const {
|
|
1571
|
+
size_t n = 0;
|
|
1572
|
+
for (size_t i = 0; i < SLOTS; ++i) n += entries_[i].used ? 1 : 0;
|
|
1573
|
+
return n;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
private:
|
|
1577
|
+
struct Entry {
|
|
1578
|
+
TSTRING host;
|
|
1579
|
+
uint32_t at = 0;
|
|
1580
|
+
uint32_t ttl = 0;
|
|
1581
|
+
bool used = false;
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
Entry* find(const TSTRING& host) {
|
|
1585
|
+
for (size_t i = 0; i < SLOTS; ++i) {
|
|
1586
|
+
if (entries_[i].used && entries_[i].host == host) return &entries_[i];
|
|
1587
|
+
}
|
|
1588
|
+
return nullptr;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
Entry entries_[SLOTS];
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1016
1594
|
/**
|
|
1017
1595
|
* @brief Gateway Acknowledgment Package for delivery confirmations
|
|
1018
1596
|
*
|
|
@@ -1116,13 +1694,40 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1116
1694
|
*/
|
|
1117
1695
|
uint32_t timestamp = 0;
|
|
1118
1696
|
|
|
1697
|
+
/**
|
|
1698
|
+
* @brief The start of the response body, summarized to one line
|
|
1699
|
+
*
|
|
1700
|
+
* Carried on success as well as failure, because whether a reply means what
|
|
1701
|
+
* the application wanted is the application's decision, not the library's.
|
|
1702
|
+
* Empty when no body was read. JSON key "resp", omitted when empty.
|
|
1703
|
+
*/
|
|
1704
|
+
TSTRING response = "";
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* @brief Whether the origin node may resend the identical request
|
|
1708
|
+
*
|
|
1709
|
+
* 1 when the request cannot have reached the server or the server asked to
|
|
1710
|
+
* be retried, 0 when a resend could deliver it twice. -1 when the gateway
|
|
1711
|
+
* did not say -- one that predates the field -- and the origin node falls
|
|
1712
|
+
* back to its own reading of the status. JSON key "retry", omitted at -1.
|
|
1713
|
+
*/
|
|
1714
|
+
int8_t retryable = -1;
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* @brief How long the server asked to wait before a retry, in milliseconds
|
|
1718
|
+
*
|
|
1719
|
+
* From a Retry-After header on a 429 or 503. JSON key "retryAfter",
|
|
1720
|
+
* omitted when 0.
|
|
1721
|
+
*/
|
|
1722
|
+
uint32_t retryAfterMs = 0;
|
|
1723
|
+
|
|
1119
1724
|
/**
|
|
1120
1725
|
* @brief Number of additional JSON fields in this package
|
|
1121
1726
|
*
|
|
1122
1727
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
1123
|
-
* Count: msgId, origin, success, http, err, ts =
|
|
1728
|
+
* Count: msgId, origin, success, http, err, ts, resp, retry, retryAfter = 9
|
|
1124
1729
|
*/
|
|
1125
|
-
static constexpr int numPackageFields =
|
|
1730
|
+
static constexpr int numPackageFields = 9;
|
|
1126
1731
|
|
|
1127
1732
|
/**
|
|
1128
1733
|
* @brief Default constructor
|
|
@@ -1149,10 +1754,19 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1149
1754
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
1150
1755
|
if (jsonObj.containsKey("err"))
|
|
1151
1756
|
error = jsonObj["err"].as<TSTRING>();
|
|
1757
|
+
if (jsonObj.containsKey("resp"))
|
|
1758
|
+
response = jsonObj["resp"].as<TSTRING>();
|
|
1759
|
+
if (jsonObj.containsKey("retry"))
|
|
1760
|
+
retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
|
|
1152
1761
|
#else
|
|
1153
1762
|
if (jsonObj["err"].is<TSTRING>())
|
|
1154
1763
|
error = jsonObj["err"].as<TSTRING>();
|
|
1764
|
+
if (jsonObj["resp"].is<TSTRING>())
|
|
1765
|
+
response = jsonObj["resp"].as<TSTRING>();
|
|
1766
|
+
if (jsonObj["retry"].is<bool>())
|
|
1767
|
+
retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
|
|
1155
1768
|
#endif
|
|
1769
|
+
retryAfterMs = jsonObj["retryAfter"] | 0UL;
|
|
1156
1770
|
}
|
|
1157
1771
|
|
|
1158
1772
|
/**
|
|
@@ -1171,6 +1785,11 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1171
1785
|
jsonObj["http"] = httpStatus;
|
|
1172
1786
|
jsonObj["err"] = error;
|
|
1173
1787
|
jsonObj["ts"] = timestamp;
|
|
1788
|
+
// Added in 2.1.0 and omitted when they say nothing, so an ack to a node
|
|
1789
|
+
// that predates them is the ack it always was.
|
|
1790
|
+
if (response.length() > 0) jsonObj["resp"] = response;
|
|
1791
|
+
if (retryable >= 0) jsonObj["retry"] = retryable == 1;
|
|
1792
|
+
if (retryAfterMs > 0) jsonObj["retryAfter"] = retryAfterMs;
|
|
1174
1793
|
return jsonObj;
|
|
1175
1794
|
}
|
|
1176
1795
|
|
|
@@ -1184,7 +1803,8 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1184
1803
|
*/
|
|
1185
1804
|
size_t jsonObjectSize() const {
|
|
1186
1805
|
// noJsonFields (from base class) + numPackageFields (our fields)
|
|
1187
|
-
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length()
|
|
1806
|
+
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length() +
|
|
1807
|
+
response.length();
|
|
1188
1808
|
}
|
|
1189
1809
|
#endif
|
|
1190
1810
|
|
|
@@ -1262,13 +1882,15 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1262
1882
|
* GATEWAY_DNS_TIMEOUT_MS on ESP8266, skipped on ESP32) runs before them
|
|
1263
1883
|
* whenever the GATEWAY_CONNECTIVITY_CACHE_MS window has expired.
|
|
1264
1884
|
*
|
|
1265
|
-
* @warning One residual is not in this budget:
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
* network with blackholed DNS the request path can
|
|
1270
|
-
* exceed this ceiling on ESP32
|
|
1271
|
-
*
|
|
1885
|
+
* @warning One residual is not in this budget: hostname resolution on ESP32
|
|
1886
|
+
* is not separately boundable in the cores this library targets.
|
|
1887
|
+
* Since issue #453 the handler performs that lookup itself on
|
|
1888
|
+
* ESP32 and remembers a failure for GATEWAY_DNS_NEGATIVE_TTL_MS, so
|
|
1889
|
+
* on a network with blackholed DNS the request path can still
|
|
1890
|
+
* exceed this ceiling on ESP32, but at most once per TTL per
|
|
1891
|
+
* destination host rather than once per attempt. On ESP8266 the
|
|
1892
|
+
* core bounds HTTPClient's own lookup by the HTTP timeout. See
|
|
1893
|
+
* SECURITY.md "Gateway blocking: the mesh partition risk".
|
|
1272
1894
|
*/
|
|
1273
1895
|
constexpr unsigned long gatewayBlockingBudgetMs() {
|
|
1274
1896
|
return 2UL * static_cast<unsigned long>(GATEWAY_HTTP_TIMEOUT_MS) +
|