@alteriom/painlessmesh 2.0.3 → 2.1.1
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 +190 -0
- package/CONTRIBUTING.md +59 -11
- package/examples/alteriom/platformio.ini +4 -1
- package/examples/basic/platformio.ini +4 -1
- package/examples/sendToInternet/README.md +54 -2
- package/examples/sendToInternet/callmebot.h +139 -0
- package/examples/sendToInternet/sendToInternet.ino +117 -39
- package/keywords.txt +1 -0
- 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 +147 -81
- package/src/boost/asynctcp.hpp +1 -1
- package/src/painlessMeshSTA.cpp +38 -4
- package/src/painlessMeshSTA.h +18 -5
- package/src/painlessmesh/buffer.hpp +22 -17
- package/src/painlessmesh/gateway.hpp +420 -73
- package/src/painlessmesh/layout.hpp +16 -0
- package/src/painlessmesh/logger.hpp +1 -1
- package/src/painlessmesh/mesh.hpp +276 -116
- package/src/painlessmesh/message_queue.hpp +6 -6
- package/src/painlessmesh/metrics.hpp +1 -1
- package/src/painlessmesh/ota.hpp +1 -1
- package/src/painlessmesh/plugin.hpp +10 -7
- package/src/painlessmesh/protocol.hpp +10 -6
- package/src/painlessmesh/rtc.hpp +3 -3
- package/src/plugin/performance.hpp +5 -5
|
@@ -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>
|
|
@@ -66,8 +67,13 @@ constexpr bool shouldFollowBridgeChannel(uint32_t localNodeId,
|
|
|
66
67
|
* @brief One occurrence of the mesh SSID seen by an all-channel scan.
|
|
67
68
|
*/
|
|
68
69
|
struct MeshChannelCandidate {
|
|
69
|
-
uint8_t channel;
|
|
70
|
-
int32_t rssi;
|
|
70
|
+
uint8_t channel = 0;
|
|
71
|
+
int32_t rssi = 0;
|
|
72
|
+
// `push_back({channel, rssi})` needs a constructor under gnu++11 (the
|
|
73
|
+
// ESP32 Arduino 2.x core), where a struct with default member
|
|
74
|
+
// initializers is not an aggregate.
|
|
75
|
+
MeshChannelCandidate() {}
|
|
76
|
+
MeshChannelCandidate(uint8_t channel_, int32_t rssi_) : channel(channel_), rssi(rssi_) {}
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
/**
|
|
@@ -884,13 +890,26 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
884
890
|
*/
|
|
885
891
|
bool requiresAck = false;
|
|
886
892
|
|
|
893
|
+
/**
|
|
894
|
+
* @brief Random value drawn once per sendToInternet() call
|
|
895
|
+
*
|
|
896
|
+
* The same on every attempt at that call, and part of its X-Request-Id and
|
|
897
|
+
* Idempotency-Key (requestIdFor()). messageId alone repeats: its counter is
|
|
898
|
+
* 16 bits, so a node that sends more than 65,535 requests in one boot
|
|
899
|
+
* reissues old ids, and a service that remembers keys would drop the new
|
|
900
|
+
* request as a repeat. 0 from a node that predates the field. JSON key
|
|
901
|
+
* "nonce", omitted when 0.
|
|
902
|
+
*/
|
|
903
|
+
uint32_t requestNonce = 0;
|
|
904
|
+
|
|
887
905
|
/**
|
|
888
906
|
* @brief Number of additional JSON fields in this package
|
|
889
907
|
*
|
|
890
908
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
891
|
-
* Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack
|
|
909
|
+
* Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack,
|
|
910
|
+
* nonce = 10 fields
|
|
892
911
|
*/
|
|
893
|
-
static constexpr int numPackageFields =
|
|
912
|
+
static constexpr int numPackageFields = 10;
|
|
894
913
|
|
|
895
914
|
/**
|
|
896
915
|
* @brief Default constructor
|
|
@@ -914,6 +933,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
914
933
|
priority = jsonObj["prio"];
|
|
915
934
|
retryCount = jsonObj["retry"];
|
|
916
935
|
requiresAck = jsonObj["ack"] | false;
|
|
936
|
+
requestNonce = jsonObj["nonce"] | 0UL;
|
|
917
937
|
|
|
918
938
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
919
939
|
if (jsonObj.containsKey("dest_url"))
|
|
@@ -951,6 +971,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
951
971
|
jsonObj["content"] = contentType;
|
|
952
972
|
jsonObj["retry"] = retryCount;
|
|
953
973
|
jsonObj["ack"] = requiresAck;
|
|
974
|
+
if (requestNonce != 0) jsonObj["nonce"] = requestNonce;
|
|
954
975
|
return jsonObj;
|
|
955
976
|
}
|
|
956
977
|
|
|
@@ -988,30 +1009,89 @@ class GatewayDataPackage : public plugin::SinglePackage {
|
|
|
988
1009
|
* @return A unique message ID
|
|
989
1010
|
*/
|
|
990
1011
|
static uint32_t generateMessageId(uint32_t nodeId) {
|
|
991
|
-
|
|
1012
|
+
// The counter starts at a random point each boot. Starting at zero, the
|
|
1013
|
+
// first request after every reboot carried the same id as the first
|
|
1014
|
+
// request of the boot before -- and so the same X-Request-Id and
|
|
1015
|
+
// Idempotency-Key, which a service that remembers keys drops as a repeat.
|
|
1016
|
+
static uint16_t counter =
|
|
1017
|
+
static_cast<uint16_t>(validation::SecureRandom::generate());
|
|
992
1018
|
++counter;
|
|
1019
|
+
if (counter == 0) ++counter;
|
|
993
1020
|
// Combine node ID (upper 16 bits) with counter (lower 16 bits)
|
|
994
1021
|
return ((nodeId & 0xFFFF) << 16) | counter;
|
|
995
1022
|
}
|
|
996
1023
|
|
|
997
1024
|
};
|
|
998
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* @brief HTTPClient transport errors, by what they say about the request
|
|
1028
|
+
*
|
|
1029
|
+
* HTTPClient::GET()/POST() return a negative code when the request failed
|
|
1030
|
+
* below HTTP. The ESP32 and ESP8266 cores number them the same way. What
|
|
1031
|
+
* matters to a retry is whether the request can have reached the server:
|
|
1032
|
+
* resending one that did delivers it twice, which for a request with an
|
|
1033
|
+
* effect -- a message, a payment, a counter -- is a second effect (the rig
|
|
1034
|
+
* showed a timed-out send issued four times).
|
|
1035
|
+
*
|
|
1036
|
+
* Where each is raised, in both cores' HTTPClient::sendRequest() for the
|
|
1037
|
+
* buffer GET()/POST() the gateway uses: -1, -2 and -3 before the request is
|
|
1038
|
+
* complete; everything else from handleHeaderResponse(), which runs after the
|
|
1039
|
+
* whole request was written -- so -4 is a connection that closed before the
|
|
1040
|
+
* reply began and -7 a reply that was not HTTP, both with the request already
|
|
1041
|
+
* at the server. -6 and -8 come from stream sends and from reading a body.
|
|
1042
|
+
*/
|
|
1043
|
+
enum HttpTransportError : int {
|
|
1044
|
+
HTTP_TRANSPORT_CONNECTION_REFUSED = -1, ///< no connection was made
|
|
1045
|
+
HTTP_TRANSPORT_SEND_HEADER_FAILED = -2, ///< the request line never completed
|
|
1046
|
+
HTTP_TRANSPORT_SEND_PAYLOAD_FAILED = -3, ///< the body never completed
|
|
1047
|
+
HTTP_TRANSPORT_NOT_CONNECTED = -4, ///< closed before the reply began; sent
|
|
1048
|
+
HTTP_TRANSPORT_CONNECTION_LOST = -5, ///< dropped while reading the reply; sent
|
|
1049
|
+
HTTP_TRANSPORT_NO_STREAM = -6, ///< no stream to send or read with
|
|
1050
|
+
HTTP_TRANSPORT_NO_HTTP_SERVER = -7, ///< the reply was not HTTP; sent
|
|
1051
|
+
HTTP_TRANSPORT_TOO_LESS_RAM = -8, ///< out of memory sending or reading
|
|
1052
|
+
HTTP_TRANSPORT_ENCODING = -9, ///< the reply arrived malformed; sent
|
|
1053
|
+
HTTP_TRANSPORT_STREAM_WRITE = -10, ///< the reply could not be stored; sent
|
|
1054
|
+
HTTP_TRANSPORT_READ_TIMEOUT = -11, ///< sent, and no reply in time
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* @brief Can a request that failed with this transport error have reached
|
|
1059
|
+
* the server?
|
|
1060
|
+
*
|
|
1061
|
+
* True -- the safe answer -- for every code that is not known to fail before
|
|
1062
|
+
* the request was complete, including codes this library does not know.
|
|
1063
|
+
*/
|
|
1064
|
+
inline bool transportErrorMayHaveReachedServer(int rawCode) {
|
|
1065
|
+
switch (rawCode) {
|
|
1066
|
+
case HTTP_TRANSPORT_CONNECTION_REFUSED:
|
|
1067
|
+
case HTTP_TRANSPORT_SEND_HEADER_FAILED:
|
|
1068
|
+
case HTTP_TRANSPORT_SEND_PAYLOAD_FAILED:
|
|
1069
|
+
return false;
|
|
1070
|
+
default:
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
999
1075
|
/**
|
|
1000
1076
|
* @brief The outcome of a gateway HTTP request, classified for the ack
|
|
1001
1077
|
*
|
|
1002
1078
|
* HTTPClient::GET()/POST() return an int with two distinct meanings: a
|
|
1003
1079
|
* 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.
|
|
1080
|
+
* negative value is one of the client's own transport errors (see
|
|
1081
|
+
* HttpTransportError). Zero is not produced by either.
|
|
1007
1082
|
*
|
|
1008
1083
|
* 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
|
|
1084
|
+
* forwarded as-is. Transport errors are reported as httpStatus 0 with the
|
|
1085
|
+
* cause in GatewayAckPackage::error, and whether the origin node may resend
|
|
1086
|
+
* the request travels in GatewayAckPackage::retryable.
|
|
1087
|
+
*
|
|
1088
|
+
* The library applies HTTP's meaning of a status and nothing else. Whether a
|
|
1089
|
+
* particular service's reply means what the application wanted -- a message
|
|
1090
|
+
* really queued, a record really written -- is the application's decision,
|
|
1091
|
+
* made from the status and the response the result carries.
|
|
1012
1092
|
*/
|
|
1013
1093
|
struct HttpRequestOutcome {
|
|
1014
|
-
/** True only for
|
|
1094
|
+
/** True only for 200, 201, 202 and 204. */
|
|
1015
1095
|
bool success = false;
|
|
1016
1096
|
|
|
1017
1097
|
/** Value to place in GatewayAckPackage::httpStatus (0 for transport errors). */
|
|
@@ -1021,39 +1101,64 @@ struct HttpRequestOutcome {
|
|
|
1021
1101
|
bool transportError = false;
|
|
1022
1102
|
|
|
1023
1103
|
/**
|
|
1024
|
-
* True
|
|
1025
|
-
*
|
|
1104
|
+
* True for a 2xx outside 200, 201, 202 and 204. 203 says a proxy
|
|
1105
|
+
* transformed the reply; 205, 206 and 208 are not what a request to an API
|
|
1106
|
+
* endpoint expects. The server answered, so it is never retried; it is
|
|
1107
|
+
* reported as a failure the application can inspect.
|
|
1026
1108
|
*/
|
|
1027
|
-
bool
|
|
1109
|
+
bool unverifiedStatus = false;
|
|
1028
1110
|
|
|
1029
1111
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1112
|
+
* Whether resending the identical request is safe and useful: the request
|
|
1113
|
+
* cannot have reached the server, or the server said it did not take it
|
|
1114
|
+
* and to come back (429 Too Many Requests, 503 Service Unavailable). A
|
|
1115
|
+
* reply that may mean the request was processed -- any 2xx, a 500, a
|
|
1116
|
+
* gateway timeout, a read timeout -- is not retried, because a retry would
|
|
1117
|
+
* be a second copy of it.
|
|
1034
1118
|
*/
|
|
1035
|
-
bool
|
|
1119
|
+
bool retryable = false;
|
|
1036
1120
|
|
|
1037
1121
|
/**
|
|
1038
|
-
* One-line
|
|
1039
|
-
*
|
|
1122
|
+
* One-line summary of the response body, for the error an application
|
|
1123
|
+
* reads; empty on success and when no body was read.
|
|
1040
1124
|
*/
|
|
1041
1125
|
TSTRING reason;
|
|
1042
1126
|
};
|
|
1043
1127
|
|
|
1044
|
-
/**
|
|
1045
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* Longest response-body summary carried in an acknowledgment. Long enough
|
|
1130
|
+
* for a service that repeats the request back before its verdict -- the
|
|
1131
|
+
* reply in issue #463 spent 97 of 120 characters on the echo and was cut
|
|
1132
|
+
* before the part that said why.
|
|
1133
|
+
*/
|
|
1134
|
+
static const size_t GATEWAY_RESPONSE_REASON_MAX = 240;
|
|
1046
1135
|
|
|
1047
1136
|
/**
|
|
1048
|
-
* How much of a response body the gateway
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1137
|
+
* How much of a response body the gateway keeps for the summary: its first
|
|
1138
|
+
* GATEWAY_RESPONSE_HEAD_BYTES and its last GATEWAY_RESPONSE_TAIL_BYTES.
|
|
1139
|
+
* Enough for a service's verdict at either end, small enough that an HTML
|
|
1140
|
+
* error page cannot eat an ESP8266's heap.
|
|
1051
1141
|
*/
|
|
1052
1142
|
static const size_t GATEWAY_RESPONSE_HEAD_BYTES = 512;
|
|
1143
|
+
static const size_t GATEWAY_RESPONSE_TAIL_BYTES = 256;
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* How far into a body the gateway reads looking for its end. Past this the
|
|
1147
|
+
* tail kept is the last of what was read, not the body's end; reading on
|
|
1148
|
+
* would hold the uplink for a reply nobody summarises.
|
|
1149
|
+
*/
|
|
1150
|
+
static const size_t GATEWAY_RESPONSE_SCAN_BYTES = 8192;
|
|
1053
1151
|
|
|
1054
|
-
/** How long the gateway
|
|
1152
|
+
/** How long the gateway reads the body for, after the status. */
|
|
1055
1153
|
static const uint32_t GATEWAY_RESPONSE_HEAD_TIMEOUT_MS = 250;
|
|
1056
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* Longest wait a server's Retry-After may impose before a retry. A longer one
|
|
1157
|
+
* is not honoured with a retry at all: the request fails and says when the
|
|
1158
|
+
* server asked to be retried, which is the application's call to make.
|
|
1159
|
+
*/
|
|
1160
|
+
static const uint32_t GATEWAY_RETRY_AFTER_MAX_MS = 60000;
|
|
1161
|
+
|
|
1057
1162
|
inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
1058
1163
|
#if defined(PAINLESSMESH_BOOST)
|
|
1059
1164
|
return haystack.find(needle) != std::string::npos;
|
|
@@ -1065,13 +1170,15 @@ inline bool responseContains(const TSTRING& haystack, const char* needle) {
|
|
|
1065
1170
|
/**
|
|
1066
1171
|
* @brief Reduce a response body to one line fit for a log or an error string
|
|
1067
1172
|
*
|
|
1068
|
-
* Tags are dropped
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1173
|
+
* Tags are dropped and whitespace collapsed, so an HTML page reads as text in
|
|
1174
|
+
* a serial log. A body longer than maxLen keeps its beginning and its end,
|
|
1175
|
+
* joined by " ... ": services often lead with an echo of the request and
|
|
1176
|
+
* finish with the verdict, and a summary that keeps only the beginning keeps
|
|
1177
|
+
* the echo and loses the verdict (issue #463).
|
|
1071
1178
|
*/
|
|
1072
1179
|
inline TSTRING summarizeResponseBody(
|
|
1073
1180
|
const TSTRING& body, size_t maxLen = GATEWAY_RESPONSE_REASON_MAX) {
|
|
1074
|
-
TSTRING
|
|
1181
|
+
TSTRING text;
|
|
1075
1182
|
bool inTag = false;
|
|
1076
1183
|
bool pendingSpace = false;
|
|
1077
1184
|
for (size_t i = 0; i < body.length(); ++i) {
|
|
@@ -1090,48 +1197,252 @@ inline TSTRING summarizeResponseBody(
|
|
|
1090
1197
|
pendingSpace = true;
|
|
1091
1198
|
continue;
|
|
1092
1199
|
}
|
|
1093
|
-
if (pendingSpace &&
|
|
1200
|
+
if (pendingSpace && text.length() > 0) text += ' ';
|
|
1094
1201
|
pendingSpace = false;
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1202
|
+
text += c;
|
|
1203
|
+
}
|
|
1204
|
+
if (text.length() <= maxLen) return text;
|
|
1205
|
+
|
|
1206
|
+
static const char JOIN[] = " ... ";
|
|
1207
|
+
const size_t joinLength = sizeof(JOIN) - 1;
|
|
1208
|
+
const size_t room = maxLen > joinLength ? maxLen - joinLength : 0;
|
|
1209
|
+
const size_t head = room / 2;
|
|
1210
|
+
const size_t tail = room - head;
|
|
1211
|
+
TSTRING out;
|
|
1212
|
+
for (size_t i = 0; i < head; ++i) out += text[i];
|
|
1213
|
+
out += JOIN;
|
|
1214
|
+
for (size_t i = text.length() - tail; i < text.length(); ++i) out += text[i];
|
|
1215
|
+
return out;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* @brief The start and the end of a response body, read a byte at a time
|
|
1220
|
+
*
|
|
1221
|
+
* The gateway cannot keep a whole body, and a service may put its verdict at
|
|
1222
|
+
* either end: CallMeBot echoes the request first and says why last (#463), so
|
|
1223
|
+
* a reply of a kilobyte kept as its first 512 bytes had lost the part that
|
|
1224
|
+
* mattered before any summary ran. This keeps the first
|
|
1225
|
+
* GATEWAY_RESPONSE_HEAD_BYTES and a rolling last GATEWAY_RESPONSE_TAIL_BYTES,
|
|
1226
|
+
* and text() joins them with " ... " when bytes were dropped between.
|
|
1227
|
+
*/
|
|
1228
|
+
class ResponseExcerpt {
|
|
1229
|
+
public:
|
|
1230
|
+
/** Keep one more byte; false once GATEWAY_RESPONSE_SCAN_BYTES were seen. */
|
|
1231
|
+
bool add(char c) {
|
|
1232
|
+
++seen_;
|
|
1233
|
+
if (head_.length() < GATEWAY_RESPONSE_HEAD_BYTES) {
|
|
1234
|
+
head_ += c;
|
|
1235
|
+
} else {
|
|
1236
|
+
tail_[(tailStart_ + tailLength_) % GATEWAY_RESPONSE_TAIL_BYTES] = c;
|
|
1237
|
+
if (tailLength_ < GATEWAY_RESPONSE_TAIL_BYTES) {
|
|
1238
|
+
++tailLength_;
|
|
1239
|
+
} else {
|
|
1240
|
+
tailStart_ = (tailStart_ + 1) % GATEWAY_RESPONSE_TAIL_BYTES;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return seen_ < GATEWAY_RESPONSE_SCAN_BYTES;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
void add(const TSTRING& text) {
|
|
1247
|
+
for (size_t i = 0; i < text.length(); ++i) {
|
|
1248
|
+
if (!add(text[i])) return;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
TSTRING text() const {
|
|
1253
|
+
TSTRING out = head_;
|
|
1254
|
+
if (seen_ > head_.length() + tailLength_) out += " ... ";
|
|
1255
|
+
for (size_t i = 0; i < tailLength_; ++i) {
|
|
1256
|
+
out += tail_[(tailStart_ + i) % GATEWAY_RESPONSE_TAIL_BYTES];
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private:
|
|
1262
|
+
TSTRING head_;
|
|
1263
|
+
char tail_[GATEWAY_RESPONSE_TAIL_BYTES] = {};
|
|
1264
|
+
size_t tailStart_ = 0;
|
|
1265
|
+
size_t tailLength_ = 0;
|
|
1266
|
+
size_t seen_ = 0;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* @brief Removes HTTP/1.1 chunked transfer framing from a body read a byte at
|
|
1271
|
+
* a time, and passes the content on to a ResponseExcerpt
|
|
1272
|
+
*
|
|
1273
|
+
* HTTPClient's getString() decodes a chunked body but keeps all of it; the
|
|
1274
|
+
* gateway reads the raw stream to keep only a bounded excerpt, and the raw
|
|
1275
|
+
* stream is the framing: CallMeBot's real reply reached the application as
|
|
1276
|
+
* "a6 Message to: ... Message queued. ... 0" (hardware rig, 2026-09-15) --
|
|
1277
|
+
* the chunk size in front, the terminating zero-length chunk behind. This
|
|
1278
|
+
* reads the framing (hex size, optional ;extensions, CRLF, data, CRLF, ...,
|
|
1279
|
+
* 0, trailers) and forwards only the data. A malformed size line stops
|
|
1280
|
+
* decoding rather than guessing: what was decoded so far is kept.
|
|
1281
|
+
*/
|
|
1282
|
+
class ChunkedBodyDecoder {
|
|
1283
|
+
public:
|
|
1284
|
+
explicit ChunkedBodyDecoder(ResponseExcerpt& excerpt) : excerpt_(excerpt) {}
|
|
1285
|
+
|
|
1286
|
+
/** One byte of the raw body; false once the body ended or the excerpt is
|
|
1287
|
+
* full, when the caller can stop reading. */
|
|
1288
|
+
bool add(char c) {
|
|
1289
|
+
switch (state_) {
|
|
1290
|
+
case State::Size:
|
|
1291
|
+
if (c == '\r') {
|
|
1292
|
+
state_ = State::SizeLf;
|
|
1293
|
+
} else if (c == ';') {
|
|
1294
|
+
state_ = State::Extension;
|
|
1295
|
+
} else if (c == ' ' || c == '\t') {
|
|
1296
|
+
// Tolerated around the size, as many servers emit it.
|
|
1297
|
+
} else {
|
|
1298
|
+
const int digit = hexValue(c);
|
|
1299
|
+
if (digit < 0 || sizeDigits_ >= 8) return stop();
|
|
1300
|
+
remaining_ = (remaining_ << 4) | static_cast<uint32_t>(digit);
|
|
1301
|
+
++sizeDigits_;
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
case State::Extension:
|
|
1305
|
+
if (c == '\r') state_ = State::SizeLf;
|
|
1306
|
+
return true;
|
|
1307
|
+
case State::SizeLf:
|
|
1308
|
+
if (c != '\n' || sizeDigits_ == 0) return stop();
|
|
1309
|
+
sizeDigits_ = 0;
|
|
1310
|
+
if (remaining_ == 0) {
|
|
1311
|
+
state_ = State::Done;
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
state_ = State::Data;
|
|
1315
|
+
return true;
|
|
1316
|
+
case State::Data:
|
|
1317
|
+
--remaining_;
|
|
1318
|
+
if (remaining_ == 0) state_ = State::DataCr;
|
|
1319
|
+
if (!excerpt_.add(c)) {
|
|
1320
|
+
state_ = State::Done;
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
return true;
|
|
1324
|
+
case State::DataCr:
|
|
1325
|
+
if (c != '\r') return stop();
|
|
1326
|
+
state_ = State::DataLf;
|
|
1327
|
+
return true;
|
|
1328
|
+
case State::DataLf:
|
|
1329
|
+
if (c != '\n') return stop();
|
|
1330
|
+
state_ = State::Size;
|
|
1331
|
+
return true;
|
|
1332
|
+
case State::Done:
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
void add(const TSTRING& raw) {
|
|
1339
|
+
for (size_t i = 0; i < raw.length(); ++i) {
|
|
1340
|
+
if (!add(raw[i])) return;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** True when the terminating zero-length chunk was read. */
|
|
1345
|
+
bool complete() const { return state_ == State::Done && !malformed_; }
|
|
1346
|
+
|
|
1347
|
+
private:
|
|
1348
|
+
enum class State { Size, Extension, SizeLf, Data, DataCr, DataLf, Done };
|
|
1349
|
+
|
|
1350
|
+
static int hexValue(char c) {
|
|
1351
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
1352
|
+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
1353
|
+
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
1354
|
+
return -1;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
bool stop() {
|
|
1358
|
+
malformed_ = true;
|
|
1359
|
+
state_ = State::Done;
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
ResponseExcerpt& excerpt_;
|
|
1364
|
+
State state_ = State::Size;
|
|
1365
|
+
uint32_t remaining_ = 0;
|
|
1366
|
+
uint8_t sizeDigits_ = 0;
|
|
1367
|
+
bool malformed_ = false;
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
/** Whether a Transfer-Encoding header value names chunked encoding. */
|
|
1371
|
+
inline bool transferEncodingIsChunked(const TSTRING& value) {
|
|
1372
|
+
TSTRING lower;
|
|
1373
|
+
for (size_t i = 0; i < value.length(); ++i) {
|
|
1374
|
+
const char c = value[i];
|
|
1375
|
+
lower += (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
|
1376
|
+
}
|
|
1377
|
+
return responseContains(lower, "chunked");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* @brief The delay a Retry-After header asks for, in milliseconds
|
|
1382
|
+
*
|
|
1383
|
+
* Only the delay-seconds form is read; an HTTP-date, or anything else, is
|
|
1384
|
+
* 0 -- no instruction -- rather than a guess. Values past
|
|
1385
|
+
* GATEWAY_RETRY_AFTER_MAX_MS are returned as they are, so the caller can see
|
|
1386
|
+
* the server asked for longer than it will wait.
|
|
1387
|
+
*/
|
|
1388
|
+
inline uint32_t parseRetryAfterMs(const TSTRING& value) {
|
|
1389
|
+
uint64_t seconds = 0;
|
|
1390
|
+
size_t digits = 0;
|
|
1391
|
+
for (size_t i = 0; i < value.length(); ++i) {
|
|
1392
|
+
const char c = value[i];
|
|
1393
|
+
if (c == ' ' || c == '\t') {
|
|
1394
|
+
if (digits == 0) continue;
|
|
1098
1395
|
break;
|
|
1099
1396
|
}
|
|
1397
|
+
if (c < '0' || c > '9') return 0;
|
|
1398
|
+
seconds = seconds * 10 + static_cast<uint64_t>(c - '0');
|
|
1399
|
+
if (seconds > 0xFFFFFFFFULL / 1000ULL) return 0xFFFFFFFFUL;
|
|
1400
|
+
++digits;
|
|
1100
1401
|
}
|
|
1101
|
-
return
|
|
1402
|
+
return digits == 0 ? 0 : static_cast<uint32_t>(seconds * 1000ULL);
|
|
1102
1403
|
}
|
|
1103
1404
|
|
|
1104
1405
|
/**
|
|
1105
|
-
* @brief
|
|
1406
|
+
* @brief The identifier the gateway sends with a request, as both
|
|
1407
|
+
* X-Request-Id and Idempotency-Key
|
|
1106
1408
|
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1409
|
+
* The same for every attempt at one sendToInternet() call, so a service that
|
|
1410
|
+
* honours Idempotency-Key treats a retry as the request it already has, and
|
|
1411
|
+
* anything recording requests can count the copies one call produced.
|
|
1412
|
+
*
|
|
1413
|
+
* The request's nonce keeps it unique beyond its messageId, whose counter
|
|
1414
|
+
* wraps after 65,535 requests in a boot. A package from a node that predates
|
|
1415
|
+
* the nonce (0) keeps the two-part form it always had.
|
|
1113
1416
|
*/
|
|
1114
|
-
inline
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1417
|
+
inline TSTRING requestIdFor(uint32_t originNode, uint32_t messageId,
|
|
1418
|
+
uint32_t requestNonce = 0) {
|
|
1419
|
+
char buffer[40];
|
|
1420
|
+
if (requestNonce == 0) {
|
|
1421
|
+
snprintf(buffer, sizeof(buffer), "pm-%08x-%08x",
|
|
1422
|
+
static_cast<unsigned>(originNode), static_cast<unsigned>(messageId));
|
|
1423
|
+
} else {
|
|
1424
|
+
snprintf(buffer, sizeof(buffer), "pm-%08x-%08x-%08x",
|
|
1425
|
+
static_cast<unsigned>(originNode), static_cast<unsigned>(messageId),
|
|
1426
|
+
static_cast<unsigned>(requestNonce));
|
|
1118
1427
|
}
|
|
1119
|
-
return
|
|
1428
|
+
return TSTRING(buffer);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/** A request nonce: random, and never the 0 that means "none". */
|
|
1432
|
+
inline uint32_t newRequestNonce() {
|
|
1433
|
+
uint32_t nonce = validation::SecureRandom::generate();
|
|
1434
|
+
return nonce != 0 ? nonce : 1;
|
|
1120
1435
|
}
|
|
1121
1436
|
|
|
1122
1437
|
/**
|
|
1123
1438
|
* @brief Classify an HTTPClient result for the gateway acknowledgment
|
|
1124
1439
|
*
|
|
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.
|
|
1440
|
+
* HTTP semantics only. 200, 201, 202 and 204 are a success. Any other 2xx is
|
|
1441
|
+
* an unverified failure the server answered; 1xx, 3xx, 4xx and 5xx are
|
|
1442
|
+
* failures. A retry is allowed only when it cannot produce a second copy of
|
|
1443
|
+
* the request: a transport error that failed before the request was complete,
|
|
1444
|
+
* or 429/503, where the server says it did not take the request. The reason
|
|
1445
|
+
* is a summary of whatever body was read, so the application learns why.
|
|
1135
1446
|
*
|
|
1136
1447
|
* @param rawCode The int returned by HTTPClient::GET() or ::POST()
|
|
1137
1448
|
* @param body The start of the response body, empty if none was read
|
|
@@ -1142,22 +1453,16 @@ inline HttpRequestOutcome classifyHttpResult(int rawCode,
|
|
|
1142
1453
|
HttpRequestOutcome outcome;
|
|
1143
1454
|
if (rawCode <= 0) {
|
|
1144
1455
|
outcome.transportError = true;
|
|
1456
|
+
outcome.retryable = !transportErrorMayHaveReachedServer(rawCode);
|
|
1145
1457
|
return outcome;
|
|
1146
1458
|
}
|
|
1147
1459
|
outcome.ackStatus =
|
|
1148
1460
|
static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
|
|
1149
|
-
|
|
1461
|
+
outcome.success =
|
|
1150
1462
|
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
|
-
}
|
|
1463
|
+
outcome.unverifiedStatus = !outcome.success && rawCode >= 200 && rawCode < 300;
|
|
1464
|
+
outcome.retryable = rawCode == 429 || rawCode == 503;
|
|
1465
|
+
if (!outcome.success) outcome.reason = summarizeResponseBody(body);
|
|
1161
1466
|
return outcome;
|
|
1162
1467
|
}
|
|
1163
1468
|
|
|
@@ -1394,13 +1699,40 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1394
1699
|
*/
|
|
1395
1700
|
uint32_t timestamp = 0;
|
|
1396
1701
|
|
|
1702
|
+
/**
|
|
1703
|
+
* @brief The start of the response body, summarized to one line
|
|
1704
|
+
*
|
|
1705
|
+
* Carried on success as well as failure, because whether a reply means what
|
|
1706
|
+
* the application wanted is the application's decision, not the library's.
|
|
1707
|
+
* Empty when no body was read. JSON key "resp", omitted when empty.
|
|
1708
|
+
*/
|
|
1709
|
+
TSTRING response = "";
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* @brief Whether the origin node may resend the identical request
|
|
1713
|
+
*
|
|
1714
|
+
* 1 when the request cannot have reached the server or the server asked to
|
|
1715
|
+
* be retried, 0 when a resend could deliver it twice. -1 when the gateway
|
|
1716
|
+
* did not say -- one that predates the field -- and the origin node falls
|
|
1717
|
+
* back to its own reading of the status. JSON key "retry", omitted at -1.
|
|
1718
|
+
*/
|
|
1719
|
+
int8_t retryable = -1;
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* @brief How long the server asked to wait before a retry, in milliseconds
|
|
1723
|
+
*
|
|
1724
|
+
* From a Retry-After header on a 429 or 503. JSON key "retryAfter",
|
|
1725
|
+
* omitted when 0.
|
|
1726
|
+
*/
|
|
1727
|
+
uint32_t retryAfterMs = 0;
|
|
1728
|
+
|
|
1397
1729
|
/**
|
|
1398
1730
|
* @brief Number of additional JSON fields in this package
|
|
1399
1731
|
*
|
|
1400
1732
|
* Used for jsonObjectSize() calculation in ArduinoJson v6.
|
|
1401
|
-
* Count: msgId, origin, success, http, err, ts =
|
|
1733
|
+
* Count: msgId, origin, success, http, err, ts, resp, retry, retryAfter = 9
|
|
1402
1734
|
*/
|
|
1403
|
-
static constexpr int numPackageFields =
|
|
1735
|
+
static constexpr int numPackageFields = 9;
|
|
1404
1736
|
|
|
1405
1737
|
/**
|
|
1406
1738
|
* @brief Default constructor
|
|
@@ -1427,10 +1759,19 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1427
1759
|
#if ARDUINOJSON_VERSION_MAJOR < 7
|
|
1428
1760
|
if (jsonObj.containsKey("err"))
|
|
1429
1761
|
error = jsonObj["err"].as<TSTRING>();
|
|
1762
|
+
if (jsonObj.containsKey("resp"))
|
|
1763
|
+
response = jsonObj["resp"].as<TSTRING>();
|
|
1764
|
+
if (jsonObj.containsKey("retry"))
|
|
1765
|
+
retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
|
|
1430
1766
|
#else
|
|
1431
1767
|
if (jsonObj["err"].is<TSTRING>())
|
|
1432
1768
|
error = jsonObj["err"].as<TSTRING>();
|
|
1769
|
+
if (jsonObj["resp"].is<TSTRING>())
|
|
1770
|
+
response = jsonObj["resp"].as<TSTRING>();
|
|
1771
|
+
if (jsonObj["retry"].is<bool>())
|
|
1772
|
+
retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
|
|
1433
1773
|
#endif
|
|
1774
|
+
retryAfterMs = jsonObj["retryAfter"] | 0UL;
|
|
1434
1775
|
}
|
|
1435
1776
|
|
|
1436
1777
|
/**
|
|
@@ -1449,6 +1790,11 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1449
1790
|
jsonObj["http"] = httpStatus;
|
|
1450
1791
|
jsonObj["err"] = error;
|
|
1451
1792
|
jsonObj["ts"] = timestamp;
|
|
1793
|
+
// Added in 2.1.0 and omitted when they say nothing, so an ack to a node
|
|
1794
|
+
// that predates them is the ack it always was.
|
|
1795
|
+
if (response.length() > 0) jsonObj["resp"] = response;
|
|
1796
|
+
if (retryable >= 0) jsonObj["retry"] = retryable == 1;
|
|
1797
|
+
if (retryAfterMs > 0) jsonObj["retryAfter"] = retryAfterMs;
|
|
1452
1798
|
return jsonObj;
|
|
1453
1799
|
}
|
|
1454
1800
|
|
|
@@ -1462,7 +1808,8 @@ class GatewayAckPackage : public plugin::SinglePackage {
|
|
|
1462
1808
|
*/
|
|
1463
1809
|
size_t jsonObjectSize() const {
|
|
1464
1810
|
// noJsonFields (from base class) + numPackageFields (our fields)
|
|
1465
|
-
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length()
|
|
1811
|
+
return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length() +
|
|
1812
|
+
response.length();
|
|
1466
1813
|
}
|
|
1467
1814
|
#endif
|
|
1468
1815
|
|