| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2022 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the GPL v2 as described in the | ||
| 10 | * included LICENSE.GPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | #include <arpa/inet.h> | ||
| 18 | #include <curl/curl.h> | ||
| 19 | #include <netinet/in.h> | ||
| 20 | #include <sys/socket.h> | ||
| 21 | #include <sys/time.h> | ||
| 22 | #include <sys/types.h> | ||
| 23 | #include <ctype.h> | ||
| 24 | #include <errno.h> | ||
| 25 | #include <netdb.h> | ||
| 26 | #include <signal.h> | ||
| 27 | |||
| 28 | #include "../Warnings.h" | ||
| 29 | #include "ChannelOutputSetup.h" | ||
| 30 | #include "../common.h" | ||
| 31 | #include "../fppversion.h" | ||
| 32 | #include "../log.h" | ||
| 33 | |||
| 34 | #include "Twinkly.h" | ||
| 35 | |||
| 36 | #include "../CurlManager.h" | ||
| 37 | #include "../CurlSocket.h" | ||
| 38 | #include "../Timers.h" | ||
| 39 | |||
| 40 | static const std::string TWINKLYTYPE = "Twinkly"; | ||
| 41 | |||
| 42 | constexpr int TOKEN_LEN = 8; | ||
| 43 | constexpr int HEADER_LEN = (1 + TOKEN_LEN + 2 + 1); | ||
| 44 | constexpr int TWINKLY_TOKEN_VALIDATE_TIME = 120; // check the token every 120s | ||
| 45 | |||
| 46 | constexpr uint32_t MAXPACKETS = 40 * 60 * 60 * 4; // about 4 hours at 40fps, | ||
| 47 | |||
| 48 | 2 | const std::string& TwinklyOutputData::GetOutputTypeString() const { | |
| 49 | 2 | return TWINKLYTYPE; | |
| 50 | } | ||
| 51 | |||
| 52 | 15 | TwinklyOutputData::TwinklyOutputData(const Json::Value& config) : | |
| 53 |
3/6✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
|
45 | UDPOutputData(config) { |
| 54 | 15 | memset((char*)&twinklyAddress, 0, sizeof(sockaddr_in)); | |
| 55 | 15 | twinklyAddress.sin_family = AF_INET; | |
| 56 | 15 | twinklyAddress.sin_port = htons(TWINKLY_PORT); | |
| 57 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | if (active) { |
| 58 |
1/2✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
|
15 | twinklyAddress.sin_addr.s_addr = toInetAddr(ipAddress, valid); |
| 59 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
|
15 | if (!valid) { |
| 60 | ✗ | AddOutputConfigWarning(14, "Could not resolve host name " + ipAddress + " - disabling output"); | |
| 61 | ✗ | active = false; | |
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | 15 | portCount = (channelCount + 899) / 900; | |
| 66 | |||
| 67 | 15 | twinklyIovecs = (struct iovec*)calloc(portCount * 2, sizeof(struct iovec)); | |
| 68 | 15 | twinklyBuffers = (uint8_t**)calloc(portCount, sizeof(unsigned char*)); | |
| 69 | |||
| 70 | 15 | int chanToOutput = channelCount; | |
| 71 | 15 | int chan = startChannel - 1; | |
| 72 |
2/2✓ Branch 0 taken 18 times.
✓ Branch 1 taken 15 times.
|
33 | for (int x = 0; x < portCount; x++) { |
| 73 | // zeroed: the token bytes go on the wire before the first auth fills them | ||
| 74 | 18 | twinklyBuffers[x] = (unsigned char*)calloc(1, HEADER_LEN); | |
| 75 | 18 | twinklyBuffers[x][0] = 3; | |
| 76 | 18 | twinklyBuffers[x][9] = 0; | |
| 77 | 18 | twinklyBuffers[x][10] = 0; | |
| 78 | 18 | twinklyBuffers[x][11] = x; | |
| 79 | |||
| 80 | // use scatter/gather for the packet. One IOV will contain | ||
| 81 | // the header, the second will point into the raw channel data | ||
| 82 | // and will be set at output time. This avoids any memcpy. | ||
| 83 | 18 | twinklyIovecs[x * 2].iov_base = twinklyBuffers[x]; | |
| 84 | 18 | twinklyIovecs[x * 2].iov_len = HEADER_LEN; | |
| 85 | 18 | twinklyIovecs[x * 2 + 1].iov_base = nullptr; | |
| 86 | 18 | twinklyIovecs[x * 2 + 1].iov_len = chanToOutput >= 900 ? 900 : chanToOutput; | |
| 87 | |||
| 88 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 13 times.
|
18 | if (chanToOutput >= 900) { |
| 89 | 5 | chanToOutput -= 900; | |
| 90 | 5 | chan += 900; | |
| 91 | } | ||
| 92 | } | ||
| 93 | 15 | } | |
| 94 | 90 | TwinklyOutputData::~TwinklyOutputData() { | |
| 95 | // Every request above captures this. Drop the ones still in flight | ||
| 96 | // instead of letting their callbacks run on a freed object; deleted on | ||
| 97 | // the main loop (a reload, or shutdown), where cancelling is allowed. | ||
| 98 | 30 | CurlManager::INSTANCE.cancelRequests(curlOwner); | |
| 99 | // The token timer captures this. StoppingOutput() normally removes it, but | ||
| 100 | // the timer must never outlive the object whatever path deleted it. Only | ||
| 101 | // while still started: the name is per address, so once stopped it may | ||
| 102 | // belong to a replacement output for the same device. | ||
| 103 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
|
30 | if (outputStarted) { |
| 104 | ✗ | Timers::INSTANCE.stopPeriodicTimer("Twinkly" + ipAddress); | |
| 105 | } | ||
| 106 |
2/2✓ Branch 0 taken 18 times.
✓ Branch 1 taken 15 times.
|
66 | for (int x = 0; x < portCount; x++) { |
| 107 | 36 | free(twinklyBuffers[x]); | |
| 108 | } | ||
| 109 | 30 | free(twinklyBuffers); | |
| 110 | 30 | free(twinklyIovecs); | |
| 111 | 60 | } | |
| 112 | |||
| 113 | 30 | void TwinklyOutputData::GetRequiredChannelRange(int& min, int& max) { | |
| 114 | // channelCount is the total across all of the device's 900 channel ports | ||
| 115 | 30 | min = startChannel - 1; | |
| 116 | 30 | max = startChannel + channelCount - 1; | |
| 117 | 30 | } | |
| 118 | |||
| 119 | 1615 | void TwinklyOutputData::PrepareData(unsigned char* channelData, UDPOutputMessages& msgs) { | |
| 120 |
3/6✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1615 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1615 times.
✗ Branch 5 not taken.
|
1615 | if (valid && active) { |
| 121 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1607 times.
|
1615 | if (tokenPending) { |
| 122 | // A new token is waiting. It is copied into the packet headers here | ||
| 123 | // rather than from the curl callback that fetched it: the headers are | ||
| 124 | // handed to the sending threads as iovecs and are only safe to touch | ||
| 125 | // from this thread, which UDPOutput::PrepData() calls holding | ||
| 126 | // socketMutex. Writing them from the main loop could put half of an | ||
| 127 | // old token and half of a new one on the wire. | ||
| 128 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::unique_lock<std::mutex> lk(authLock); |
| 129 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 8 times.
|
17 | for (int x = 0; x < portCount; x++) { |
| 130 | 9 | memcpy(&twinklyBuffers[x][1], authTokenBytes, TOKEN_LEN); | |
| 131 | } | ||
| 132 | 8 | tokenPending = false; | |
| 133 | 8 | } | |
| 134 | |||
| 135 | // No token: the login failed (the device was unreachable or still | ||
| 136 | // booting when output started) or has not finished. Retrying only | ||
| 137 | // on the 120s token check left a device that came back sooner | ||
| 138 | // getting frames it ignores; valid means it answers pings again. | ||
| 139 |
1/2✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
|
1615 | uint64_t now = GetTimeMS(); |
| 140 |
11/14✓ Branch 0 taken 1080 times.
✓ Branch 1 taken 535 times.
✓ Branch 2 taken 926 times.
✓ Branch 3 taken 154 times.
✓ Branch 4 taken 926 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✓ Branch 7 taken 921 times.
✓ Branch 8 taken 926 times.
✓ Branch 9 taken 689 times.
✓ Branch 10 taken 5 times.
✓ Branch 11 taken 1610 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
|
1615 | if (!authInFlight && now - lastAuthAttemptMS > 5000 && getAuthToken().empty()) { |
| 141 | 5 | lastAuthAttemptMS = now; | |
| 142 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | authenticate(); |
| 143 | } | ||
| 144 | |||
| 145 | 1615 | reauthCount++; | |
| 146 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1615 times.
|
1615 | if (reauthCount > MAXPACKETS) { |
| 147 | // need to re-authenticate or lights will stop eventually. If this | ||
| 148 | // attempt fails, the periodic verifyToken() will retry within | ||
| 149 | // TWINKLY_TOKEN_VALIDATE_TIME rather than waiting another MAXPACKETS. | ||
| 150 | ✗ | reauthCount = 0; | |
| 151 | ✗ | authenticate(); | |
| 152 | } | ||
| 153 | |||
| 154 | 1615 | int start = 0; | |
| 155 | struct mmsghdr msg; | ||
| 156 | 1615 | memset(&msg, 0, sizeof(msg)); | |
| 157 | |||
| 158 | 1615 | msg.msg_hdr.msg_name = &twinklyAddress; | |
| 159 | 1615 | msg.msg_hdr.msg_namelen = sizeof(sockaddr_in); | |
| 160 | 1615 | bool anySkipped = false; | |
| 161 | 1615 | bool allSkipped = true; | |
| 162 |
2/2✓ Branch 0 taken 1644 times.
✓ Branch 1 taken 1615 times.
|
3259 | for (int p = 0; p < portCount; p++) { |
| 163 |
1/2✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
|
1644 | bool nto = NeedToOutputFrame(channelData, startChannel - 1, start, twinklyIovecs[p * 2 + 1].iov_len); |
| 164 |
1/2✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
|
1644 | if (nto) { |
| 165 | 1644 | msg.msg_hdr.msg_iov = &twinklyIovecs[p * 2]; | |
| 166 | 1644 | msg.msg_hdr.msg_iovlen = 2; | |
| 167 | 1644 | msg.msg_len = twinklyIovecs[p * 2 + 1].iov_len + twinklyIovecs[p * 2].iov_len; | |
| 168 | |||
| 169 |
2/4✓ Branch 0 taken 1644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1644 times.
✗ Branch 3 not taken.
|
1644 | msgs[twinklyAddress.sin_addr.s_addr].push_back(msg); |
| 170 | |||
| 171 | // set the pointer to the channelData for the universe | ||
| 172 | 1644 | twinklyIovecs[p * 2 + 1].iov_base = (void*)(&channelData[startChannel - 1 + start]); | |
| 173 | 1644 | allSkipped = false; | |
| 174 | } else { | ||
| 175 | ✗ | anySkipped = true; | |
| 176 | } | ||
| 177 | 1644 | start += twinklyIovecs[p * 2 + 1].iov_len; | |
| 178 | } | ||
| 179 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1615 times.
|
1615 | if (anySkipped) { |
| 180 | ✗ | skippedFrames++; | |
| 181 | } else { | ||
| 182 | 1615 | skippedFrames = 0; | |
| 183 | } | ||
| 184 |
1/2✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
|
1615 | if (!allSkipped) { |
| 185 |
1/2✓ Branch 0 taken 1615 times.
✗ Branch 1 not taken.
|
1615 | SaveFrame(&channelData[startChannel - 1], start); |
| 186 | } | ||
| 187 | } | ||
| 188 | 1615 | } | |
| 189 | |||
| 190 | 17 | void TwinklyOutputData::StartingOutput() { | |
| 191 | 17 | outputStarted = true; | |
| 192 | 17 | authenticate(); | |
| 193 |
2/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
17 | Timers::INSTANCE.addPeriodicTimer("Twinkly" + ipAddress, TWINKLY_TOKEN_VALIDATE_TIME * 1000, [this]() { |
| 194 | ✗ | verifyToken(); | |
| 195 | ✗ | }); | |
| 196 | 17 | } | |
| 197 | 17 | void TwinklyOutputData::StoppingOutput() { | |
| 198 | // Clear this first. An authentication chain may be part way through on the | ||
| 199 | // main loop; without it, the chain would finish after we are done here and | ||
| 200 | // put the device back into "rt" mode with nothing left to send it frames. | ||
| 201 | 17 | outputStarted = false; | |
| 202 |
2/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
17 | Timers::INSTANCE.stopPeriodicTimer("Twinkly" + ipAddress); |
| 203 | // This runs on the output thread or under outputThreadLock on a reload, | ||
| 204 | // and the call blocks for up to 5s. Only make it for a device that is | ||
| 205 | // answering and that this output put into rt mode; one whose ping failed | ||
| 206 | // is stopped here too, and would only time out. | ||
| 207 |
9/12✓ Branch 0 taken 16 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 9 times.
✓ Branch 6 taken 16 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 7 times.
✓ Branch 9 taken 10 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
17 | if (valid && !getAuthToken().empty()) { |
| 208 |
3/6✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
|
28 | callRestAPI(true, "xled/v1/led/mode", "{\"mode\": \"off\"}"); |
| 209 | } | ||
| 210 |
2/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
17 | setAuthToken(""); |
| 211 | 17 | } | |
| 212 | |||
| 213 | 949 | std::string TwinklyOutputData::getAuthToken() { | |
| 214 |
1/2✓ Branch 0 taken 949 times.
✗ Branch 1 not taken.
|
949 | std::unique_lock<std::mutex> lk(authLock); |
| 215 |
1/2✓ Branch 0 taken 949 times.
✗ Branch 1 not taken.
|
1898 | return authToken; |
| 216 | 949 | } | |
| 217 | 17 | void TwinklyOutputData::setAuthToken(const std::string& token) { | |
| 218 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | std::unique_lock<std::mutex> lk(authLock); |
| 219 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | authToken = token; |
| 220 | 17 | } | |
| 221 | |||
| 222 | 8 | bool TwinklyOutputData::applyAuthToken(const std::string& token) { | |
| 223 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::vector<uint8_t> decoded = base64Decode(token); |
| 224 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (decoded.empty()) { |
| 225 | ✗ | return false; | |
| 226 | } | ||
| 227 | 8 | int len = std::min(TOKEN_LEN, (int)decoded.size()); | |
| 228 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | std::unique_lock<std::mutex> lk(authLock); |
| 229 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | authToken = token; |
| 230 | // A short token leaves the rest of the header zeroed rather than carrying | ||
| 231 | // bytes of the previous one. | ||
| 232 | 8 | memset(authTokenBytes, 0, TOKEN_LEN); | |
| 233 | 8 | memcpy(authTokenBytes, &decoded[0], len); | |
| 234 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | lk.unlock(); |
| 235 | 8 | tokenPending = true; | |
| 236 | 8 | return true; | |
| 237 | 8 | } | |
| 238 | |||
| 239 | 18 | void TwinklyOutputData::finishAuth() { | |
| 240 | 18 | authInFlight = false; | |
| 241 |
2/6✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
|
18 | if (authPending.exchange(false) && outputStarted) { |
| 242 | ✗ | authenticate(); | |
| 243 | } | ||
| 244 | 18 | } | |
| 245 | |||
| 246 | // The xled API reports failures in the body, not the status line: a rejected | ||
| 247 | // token comes back as HTTP 200 with {"code":1102}, so the response code on its | ||
| 248 | // own never shows an error. | ||
| 249 | 17 | static bool xledCallSucceeded(int rc, const std::string& resp) { | |
| 250 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
|
17 | if (rc != 200) { |
| 251 | ✗ | return false; | |
| 252 | } | ||
| 253 | try { | ||
| 254 |
1/2✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
|
17 | Json::Value v = LoadJsonFromString(resp); |
| 255 |
2/4✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
|
17 | return v["code"].asInt() == 1000; |
| 256 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
17 | } catch (std::exception& ex) { |
| 257 | ✗ | return false; | |
| 258 | ✗ | } | |
| 259 | } | ||
| 260 | |||
| 261 | 22 | void TwinklyOutputData::authenticate() { | |
| 262 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (!outputStarted) { |
| 263 | ✗ | return; | |
| 264 | } | ||
| 265 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (authInFlight.exchange(true)) { |
| 266 | // One is already running. Remember this one instead of dropping it: | ||
| 267 | // the in-flight attempt may be about to fail, or may have been started | ||
| 268 | // before a stop/start cycle invalidated what it is fetching. | ||
| 269 | ✗ | authPending = true; | |
| 270 | ✗ | return; | |
| 271 | } | ||
| 272 | |||
| 273 |
2/4✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
|
22 | const std::string base = "http://" + ipAddress + "/xled/v1/"; |
| 274 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
66 | const std::list<std::string> jsonHeaders = { "Accept: application/json", "Content-Type: application/json" }; |
| 275 | |||
| 276 |
5/10✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 22 times.
✗ Branch 9 not taken.
|
154 | CurlManager::INSTANCE.add( |
| 277 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
44 | base + "login", "POST", |
| 278 | "{\"challenge\": \"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\"}", jsonHeaders, | ||
| 279 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
22 | [this, base](int rc, const std::string& resp) { |
| 280 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17 times.
|
18 | if (!outputStarted) { |
| 281 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | finishAuth(); |
| 282 | 9 | return; | |
| 283 | } | ||
| 284 | 17 | std::string token; | |
| 285 |
2/2✓ Branch 0 taken 9 times.
✓ Branch 1 taken 8 times.
|
17 | if (rc == 200) { |
| 286 | try { | ||
| 287 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | Json::Value v = LoadJsonFromString(resp); |
| 288 |
5/14✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 9 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
|
9 | token = v.isMember("authentication_token") ? v["authentication_token"].asString() : ""; |
| 289 |
0/2✗ Branch 0 not taken.
✗ Branch 1 not taken.
|
9 | } catch (std::exception& ex) { |
| 290 | // fall through with an empty token and try again next cycle | ||
| 291 | ✗ | } | |
| 292 | } | ||
| 293 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 9 times.
|
17 | if (token.empty()) { |
| 294 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | LogWarn(VB_CHANNELOUT, "Twinkly %s: login failed (http %d), will retry\n", ipAddress.c_str(), rc); |
| 295 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | finishAuth(); |
| 296 | 8 | return; | |
| 297 | } | ||
| 298 |
2/4✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
|
9 | if (base64Decode(token).empty()) { |
| 299 | ✗ | LogWarn(VB_CHANNELOUT, "Twinkly %s: could not decode the authentication token, will retry\n", ipAddress.c_str()); | |
| 300 | ✗ | finishAuth(); | |
| 301 | ✗ | return; | |
| 302 | } | ||
| 303 | |||
| 304 | // The token is carried explicitly through the chain rather than read | ||
| 305 | // back off the member: another attempt may replace it in between. | ||
| 306 | // POST is what every shipping release has used to verify - some units | ||
| 307 | // answer a GET here too, but not all firmware routes it. | ||
| 308 |
3/6✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
27 | const std::list<std::string> authHeader = { "X-Auth-Token: " + token }; |
| 309 |
7/18✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 9 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
|
54 | CurlManager::INSTANCE.add(base + "verify", "POST", "", authHeader, [this, base, token](int rc, const std::string& resp) { |
| 310 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
|
9 | if (!outputStarted) { |
| 311 | ✗ | finishAuth(); | |
| 312 | 1 | return; | |
| 313 | } | ||
| 314 |
3/4✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 8 times.
|
9 | if (!xledCallSucceeded(rc, resp)) { |
| 315 | // Carrying on would put the device into "rt" with a token it | ||
| 316 | // has not accepted, and every frame after that is dropped. | ||
| 317 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogWarn(VB_CHANNELOUT, "Twinkly %s: token was not verified (http %d), will retry\n", ipAddress.c_str(), rc); |
| 318 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | finishAuth(); |
| 319 | 1 | return; | |
| 320 | } | ||
| 321 | // Only a token the device accepted becomes ours. Installing it | ||
| 322 | // at login left a refused one in place, which stopped | ||
| 323 | // PrepareData()'s no-token retry from ever logging in again. | ||
| 324 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | applyAuthToken(token); |
| 325 | const std::list<std::string> modeHeaders = { "Accept: application/json", | ||
| 326 | "Content-Type: application/json", | ||
| 327 |
3/6✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
48 | "X-Auth-Token: " + token }; |
| 328 |
5/10✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
|
40 | CurlManager::INSTANCE.add(base + "led/mode", "POST", "{\"mode\": \"rt\"}", modeHeaders, |
| 329 | 8 | [this](int rc, const std::string& resp) { | |
| 330 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
8 | if (!xledCallSucceeded(rc, resp)) { |
| 331 | ✗ | LogWarn(VB_CHANNELOUT, "Twinkly %s: could not switch to realtime mode (http %d)\n", ipAddress.c_str(), rc); | |
| 332 | } | ||
| 333 | 8 | finishAuth(); | |
| 334 | 8 | }, | |
| 335 | 8 | curlOwner); | |
| 336 |
3/10✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
24 | }, |
| 337 |
1/2✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
|
9 | curlOwner); |
| 338 |
3/8✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
|
26 | }, |
| 339 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | curlOwner); |
| 340 | 22 | } | |
| 341 | ✗ | void TwinklyOutputData::verifyToken() { | |
| 342 | ✗ | if (!outputStarted) { | |
| 343 | ✗ | return; | |
| 344 | } | ||
| 345 | ✗ | std::string url = "http://" + ipAddress + "/xled/v1/verify"; | |
| 346 | ✗ | std::list<std::string> extraHeaders; | |
| 347 | ✗ | std::string xat = "X-Auth-Token: " + getAuthToken(); | |
| 348 | ✗ | extraHeaders.push_back(xat); | |
| 349 | ✗ | CurlManager::INSTANCE.add(url, "POST", "", extraHeaders, [this](int rc, const std::string& resp) { | |
| 350 | // printf("%d: %s\n", rc, resp.c_str()); | ||
| 351 | ✗ | if (!xledCallSucceeded(rc, resp)) { | |
| 352 | // no answer, or the token was rejected. Retry async | ||
| 353 | ✗ | authenticate(); | |
| 354 | } | ||
| 355 | ✗ | }, | |
| 356 | ✗ | curlOwner); | |
| 357 | ✗ | } | |
| 358 | |||
| 359 | // URL Helpers | ||
| 360 | ✗ | size_t urlWriteData(void* buffer, size_t size, size_t nmemb, void* userp) { | |
| 361 | ✗ | std::string* str = (std::string*)userp; | |
| 362 | |||
| 363 | ✗ | str->append(static_cast<const char*>(buffer), size * nmemb); | |
| 364 | |||
| 365 | ✗ | return size * nmemb; | |
| 366 | } | ||
| 367 | 7 | Json::Value TwinklyOutputData::callRestAPI(bool isPost, const std::string& path, const std::string& data) { | |
| 368 |
3/6✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
|
7 | std::string url = "http://" + ipAddress + "/" + path; |
| 369 | |||
| 370 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | CURL* curl = curl_easy_init(); |
| 371 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | SetCurlCloseOnExec(curl); |
| 372 | 7 | struct curl_slist* headers = NULL; | |
| 373 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | std::string userAgent = "FPP/"; |
| 374 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
|
7 | userAgent += getFPPVersionTriplet(); |
| 375 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | std::string resp = ""; |
| 376 | |||
| 377 | CURLcode status; | ||
| 378 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, urlWriteData); |
| 379 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); |
| 380 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); |
| 381 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); |
| 382 |
11/30✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 7 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 7 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 7 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 7 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 7 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 7 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 7 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
|
35 | if (startsWith(data, "{") && endsWith(data, "}")) { |
| 383 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | headers = curl_slist_append(headers, "Accept: application/json"); |
| 384 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | headers = curl_slist_append(headers, "Content-Type: application/json"); |
| 385 | } | ||
| 386 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | const std::string token = getAuthToken(); |
| 387 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
|
7 | if (token != "") { |
| 388 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | const std::string at = "X-Auth-Token: " + token; |
| 389 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | headers = curl_slist_append(headers, at.c_str()); |
| 390 | 7 | } | |
| 391 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | if (headers) { |
| 392 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); |
| 393 | } | ||
| 394 | |||
| 395 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | if (isPost) { |
| 396 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); |
| 397 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_POST, 1); |
| 398 | } | ||
| 399 | |||
| 400 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)5); |
| 401 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2); |
| 402 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); |
| 403 | |||
| 404 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | status = curl_easy_perform(curl); |
| 405 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
|
7 | if (status != CURLE_OK) { |
| 406 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
5 | LogErr(VB_GENERAL, "curl_easy_perform() failed: %s\n", curl_easy_strerror(status)); |
| 407 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | curl_easy_cleanup(curl); |
| 408 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | curl_slist_free_all(headers); |
| 409 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | return Json::Value(); |
| 410 | } | ||
| 411 | |||
| 412 | // printf("%s: %s\n", url.c_str(), resp.c_str()); | ||
| 413 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | LogDebug(VB_GENERAL, "%s %s resp: %s\n", isPost ? "POST" : "GET", url.c_str(), resp.c_str()); |
| 414 | |||
| 415 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | curl_easy_cleanup(curl); |
| 416 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | curl_slist_free_all(headers); |
| 417 | |||
| 418 | try { | ||
| 419 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | return LoadJsonFromString(resp); |
| 420 | ✗ | } catch (std::exception& ex) { | |
| 421 | // not sure what to do here. The only call that uses the return is the authentication | ||
| 422 | // so we'll return an empty json and hope that is good enough | ||
| 423 | ✗ | return Json::Value(); | |
| 424 | ✗ | } | |
| 425 | 7 | } | |
| 426 | |||
| 427 | 15 | void TwinklyOutputData::DumpConfig() { | |
| 428 | 15 | LogDebug(VB_CHANNELOUT, "Twinkly: %s %d:%d:%d:%d %s\n", | |
| 429 | description.c_str(), | ||
| 430 | active, | ||
| 431 | startChannel, | ||
| 432 | channelCount, | ||
| 433 | type, | ||
| 434 | ipAddress.c_str()); | ||
| 435 | 15 | } | |
| 436 |