@alteriom/painlessmesh 2.0.3 → 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 +123 -0
- package/CONTRIBUTING.md +51 -8
- package/examples/sendToInternet/README.md +54 -2
- package/examples/sendToInternet/callmebot.h +139 -0
- package/examples/sendToInternet/sendToInternet.ino +117 -39
- 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 +78 -62
- package/src/painlessMeshSTA.cpp +38 -4
- package/src/painlessMeshSTA.h +13 -1
- package/src/painlessmesh/buffer.hpp +11 -6
- package/src/painlessmesh/gateway.hpp +413 -71
- package/src/painlessmesh/layout.hpp +16 -0
- package/src/painlessmesh/mesh.hpp +251 -106
|
@@ -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>
|
|
@@ -884,13 +885,26 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
884
885
|
*/
|
|
885
886
|
bool requiresAck = false;
|
|
886
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
|
+
|
|
887
900
|
/**
|
|
888
901
|
* @brief Number of additional JSON fields in this package
|
|
889
902
|
*
|
|
890
903
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
891
|
-
* 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
|
|
892
906
|
*/
|
|
893
|
-
static constexpr int numPackageFields =
|
|
907
|
+
static constexpr int numPackageFields = 10;
|
|
894
908
|
|
|
895
909
|
/**
|
|
896
910
|
* @brief Default constructor
|
|
@@ -914,6 +928,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
914
928
|
priority = jsonObj["prio"];
|
|
915
929
|
retryCount = jsonObj["retry"];
|
|
916
930
|
requiresAck = jsonObj["ack"] | false;
|
|
931
|
+
requestNonce = jsonObj["nonce"] | 0UL;
|
|
917
932
|
|
|
918
933
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
919
934
|
if (jsonObj.containsKey("dest_url"))
|
|
@@ -951,6 +966,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
951
966
|
jsonObj["content"] = contentType;
|
|
952
967
|
jsonObj["retry"] = retryCount;
|
|
953
968
|
jsonObj["ack"] = requiresAck;
|
|
969
|
+
if (requestNonce != 0) jsonObj["nonce"] = requestNonce;
|
|
954
970
|
return jsonObj;
|
|
955
971
|
}
|
|
956
972
|
|
|
@@ -988,30 +1004,89 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
988
1004
|
* @return A unique message ID
|
|
989
1005
|
*/
|
|
990
1006
|
static uint32_t generateMessageId(uint32_t nodeId) {
|
|
991
|
-
|
|
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());
|
|
992
1013
|
++counter;
|
|
1014
|
+
if (counter == 0) ++counter;
|
|
993
1015
|
// Combine node ID (upper 16 bits) with counter (lower 16 bits)
|
|
994
1016
|
return ((nodeId & 0xFFFF) << 16) | counter;
|
|
995
1017
|
}
|
|
996
1018
|
|
|
997
1019
|
};
|
|
998
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
|
+
|
|
999
1070
|
/**
|
|
1000
1071
|
* @brief The outcome of a gateway HTTP request, classified for the ack
|
|
1001
1072
|
*
|
|
1002
1073
|
* HTTPClient::GET()/POST() return an int with two distinct meanings: a
|
|
1003
1074
|
* positive value is an HTTP status code from the destination, while a
|
|
1004
|
-
* negative value is one of the client's own transport errors (
|
|
1005
|
-
*
|
|
1006
|
-
* produced by either.
|
|
1075
|
+
* negative value is one of the client's own transport errors (see
|
|
1076
|
+
* HttpTransportError). Zero is not produced by either.
|
|
1007
1077
|
*
|
|
1008
1078
|
* GatewayAckPackage::httpStatus is a uint16_t, so a negative code cannot be
|
|
1009
|
-
* forwarded as-is. Transport errors are reported as httpStatus 0
|
|
1010
|
-
*
|
|
1011
|
-
* 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.
|
|
1012
1087
|
*/
|
|
1013
1088
|
struct HttpRequestOutcome {
|
|
1014
|
-
/** True only for
|
|
1089
|
+
/** True only for 200, 201, 202 and 204. */
|
|
1015
1090
|
bool success = false;
|
|
1016
1091
|
|
|
1017
1092
|
/** Value to place in GatewayAckPackage::httpStatus (0 for transport errors). */
|
|
@@ -1021,39 +1096,64 @@ struct HttpRequestOutcome {
|
|
|
1021
1096
|
bool transportError = false;
|
|
1022
1097
|
|
|
1023
1098
|
/**
|
|
1024
|
-
* True
|
|
1025
|
-
*
|
|
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.
|
|
1026
1103
|
*/
|
|
1027
|
-
bool
|
|
1104
|
+
bool unverifiedStatus = false;
|
|
1028
1105
|
|
|
1029
1106
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
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.
|
|
1034
1113
|
*/
|
|
1035
|
-
bool
|
|
1114
|
+
bool retryable = false;
|
|
1036
1115
|
|
|
1037
1116
|
/**
|
|
1038
|
-
* One-line
|
|
1039
|
-
*
|
|
1117
|
+
* One-line summary of the response body, for the error an application
|
|
1118
|
+
* reads; empty on success and when no body was read.
|
|
1040
1119
|
*/
|
|
1041
1120
|
TSTRING reason;
|
|
1042
1121
|
};
|
|
1043
1122
|
|
|
1044
|
-
/**
|
|
1045
|
-
|
|
1123
|
+
/**
|
|
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;
|
|
1046
1130
|
|
|
1047
1131
|
/**
|
|
1048
|
-
* How much of a response body the gateway
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
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.
|
|
1051
1136
|
*/
|
|
1052
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;
|
|
1053
1146
|
|
|
1054
|
-
/** How long the gateway
|
|
1147
|
+
/** How long the gateway reads the body for, after the status. */
|
|
1055
1148
|
static const uint32_t GATEWAY_RESPONSE_HEAD_TIMEOUT_MS = 250;
|
|
1056
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
|
+
|
|
1057
1157
|
inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
1058
1158
|
#if defined(PAINLESSMESH_BOOST)
|
|
1059
1159
|
return haystack.find(needle) != std::string::npos;
|
|
@@ -1065,13 +1165,15 @@ inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
|
1065
1165
|
/**
|
|
1066
1166
|
* @brief Reduce a response body to one line fit for a log or an error string
|
|
1067
1167
|
*
|
|
1068
|
-
* Tags are dropped
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
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).
|
|
1071
1173
|
*/
|
|
1072
1174
|
inline TSTRING summarizeResponseBody(
|
|
1073
1175
|
const TSTRING& body, size_t maxLen = GATEWAY_RESPONSE_REASON_MAX) {
|
|
1074
|
-
TSTRING
|
|
1176
|
+
TSTRING text;
|
|
1075
1177
|
bool inTag = false;
|
|
1076
1178
|
bool pendingSpace = false;
|
|
1077
1179
|
for (size_t i = 0; i < body.length(); ++i) {
|
|
@@ -1090,48 +1192,252 @@ inline TSTRING summarizeResponseBody(
|
|
|
1090
1192
|
pendingSpace = true;
|
|
1091
1193
|
continue;
|
|
1092
1194
|
}
|
|
1093
|
-
if (pendingSpace &&
|
|
1195
|
+
if (pendingSpace && text.length() > 0) text += ' ';
|
|
1094
1196
|
pendingSpace = false;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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;
|
|
1098
1390
|
break;
|
|
1099
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;
|
|
1100
1396
|
}
|
|
1101
|
-
return
|
|
1397
|
+
return digits == 0 ? 0 : static_cast<uint32_t>(seconds * 1000ULL);
|
|
1102
1398
|
}
|
|
1103
1399
|
|
|
1104
1400
|
/**
|
|
1105
|
-
* @brief
|
|
1401
|
+
* @brief The identifier the gateway sends with a request, as both
|
|
1402
|
+
* X-Request-Id and Idempotency-Key
|
|
1106
1403
|
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
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.
|
|
1113
1411
|
*/
|
|
1114
|
-
inline
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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));
|
|
1118
1422
|
}
|
|
1119
|
-
return
|
|
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;
|
|
1120
1430
|
}
|
|
1121
1431
|
|
|
1122
1432
|
/**
|
|
1123
1433
|
* @brief Classify an HTTPClient result for the gateway acknowledgment
|
|
1124
1434
|
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
* of them is a delivery this library can vouch for, so they are failures
|
|
1132
|
-
* that carry the body. Every failure's reason is a one-line excerpt of the
|
|
1133
|
-
* body when there is one, so the origin node learns why and not only a
|
|
1134
|
-
* number.
|
|
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.
|
|
1135
1441
|
*
|
|
1136
1442
|
* @param rawCode The int returned by HTTPClient::GET() or ::POST()
|
|
1137
1443
|
* @param body The start of the response body, empty if none was read
|
|
@@ -1142,22 +1448,16 @@ inline HttpRequestOutcome classifyHttpResult(int rawCode,
|
|
|
1142
1448
|
HttpRequestOutcome outcome;
|
|
1143
1449
|
if (rawCode <= 0) {
|
|
1144
1450
|
outcome.transportError = true;
|
|
1451
|
+
outcome.retryable = !transportErrorMayHaveReachedServer(rawCode);
|
|
1145
1452
|
return outcome;
|
|
1146
1453
|
}
|
|
1147
1454
|
outcome.ackStatus =
|
|
1148
1455
|
static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
|
|
1149
|
-
|
|
1456
|
+
outcome.success =
|
|
1150
1457
|
rawCode == 200 || rawCode == 201 || rawCode == 202 || rawCode == 204;
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
return outcome;
|
|
1155
|
-
}
|
|
1156
|
-
outcome.success = verified;
|
|
1157
|
-
if (!verified) {
|
|
1158
|
-
outcome.unverifiedStatus = rawCode >= 200 && rawCode < 300;
|
|
1159
|
-
outcome.reason = summarizeResponseBody(body);
|
|
1160
|
-
}
|
|
1458
|
+
outcome.unverifiedStatus = !outcome.success && rawCode >= 200 && rawCode < 300;
|
|
1459
|
+
outcome.retryable = rawCode == 429 || rawCode == 503;
|
|
1460
|
+
if (!outcome.success) outcome.reason = summarizeResponseBody(body);
|
|
1161
1461
|
return outcome;
|
|
1162
1462
|
}
|
|
1163
1463
|
|
|
@@ -1394,13 +1694,40 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1394
1694
|
*/
|
|
1395
1695
|
uint32_t timestamp = 0;
|
|
1396
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
|
+
|
|
1397
1724
|
/**
|
|
1398
1725
|
* @brief Number of additional JSON fields in this package
|
|
1399
1726
|
*
|
|
1400
1727
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
1401
|
-
* Count: msgId, origin, success, http, err, ts =
|
|
1728
|
+
* Count: msgId, origin, success, http, err, ts, resp, retry, retryAfter = 9
|
|
1402
1729
|
*/
|
|
1403
|
-
static constexpr int numPackageFields =
|
|
1730
|
+
static constexpr int numPackageFields = 9;
|
|
1404
1731
|
|
|
1405
1732
|
/**
|
|
1406
1733
|
* @brief Default constructor
|
|
@@ -1427,10 +1754,19 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1427
1754
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
1428
1755
|
if (jsonObj.containsKey("err"))
|
|
1429
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;
|
|
1430
1761
|
#else
|
|
1431
1762
|
if (jsonObj["err"].is<TSTRING>())
|
|
1432
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;
|
|
1433
1768
|
#endif
|
|
1769
|
+
retryAfterMs = jsonObj["retryAfter"] | 0UL;
|
|
1434
1770
|
}
|
|
1435
1771
|
|
|
1436
1772
|
/**
|
|
@@ -1449,6 +1785,11 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1449
1785
|
jsonObj["http"] = httpStatus;
|
|
1450
1786
|
jsonObj["err"] = error;
|
|
1451
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;
|
|
1452
1793
|
return jsonObj;
|
|
1453
1794
|
}
|
|
1454
1795
|
|
|
@@ -1462,7 +1803,8 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1462
1803
|
*/
|
|
1463
1804
|
size_t jsonObjectSize() const {
|
|
1464
1805
|
// noJsonFields (from base class) + numPackageFields (our fields)
|
|
1465
|
-
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length()
|
|
1806
|
+
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length() +
|
|
1807
|
+
response.length();
|
|
1466
1808
|
}
|
|
1467
1809
|
#endif
|
|
1468
1810
|
|
|
@@ -127,6 +127,22 @@ class Layout {
|
|
|
127
127
|
bool hasTimeAuthority = false;
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
/** How many of this node's connections are up.
|
|
131
|
+
*
|
|
132
|
+
* Zero means the node is out of the mesh: it has no route to anywhere, and
|
|
133
|
+
* nothing it is told about the topology can be acted on. A closed
|
|
134
|
+
* connection stays in `subs` until eraseClosedConnections() next runs, and
|
|
135
|
+
* a count of `subs` therefore says a node is connected when it is not --
|
|
136
|
+
* which is the same trap findRoute() was fixed for.
|
|
137
|
+
*/
|
|
138
|
+
template <class T>
|
|
139
|
+
size_t liveSubs(const Layout<T>& layout) {
|
|
140
|
+
size_t live = 0;
|
|
141
|
+
for (auto&& sub : layout.subs)
|
|
142
|
+
if (sub->connected()) ++live;
|
|
143
|
+
return live;
|
|
144
|
+
}
|
|
145
|
+
|
|
130
146
|
template <class T>
|
|
131
147
|
void syncLayout(Layout<T>& layout, uint32_t changedId) {
|
|
132
148
|
// TODO: this should be called from changed connections and dropped
|