1 ///
2 /// Pandora communication library.
3 /// @file       mediaunits/pandora/pandoratypes.h - pianod project
4 /// @author     Perette Barella
5 /// @date       2020-03-23
6 /// @copyright  Copyright 2020 Devious Fish. All rights reserved.
7 ///
8 
9 #include <config.h>
10 
11 #include <string>
12 
13 #include <cctype>
14 
15 #include <curl/curl.h>
16 
17 #include "logging.h"
18 
19 #include "pandoramessages.h"
20 #include "pandoracomm.h"
21 
22 namespace Pandora {
23     /// Pandora REST API location
24     const std::string Communication::EndpointUrl = "https://www.pandora.com/api/";
25 
26     namespace Key {
27         static const char *CSRFToken = "csrfToken";
28     }
29 
30     /** Return a string corresponding to a Pandora communication status.
31         @param status The communication status.
32         @return The corresponding string. */
status_strerror(Status status)33     const std::string status_strerror (Status status) {
34         switch (status) {
35             case Status::Ok:
36                 return "Ok";
37             case Status::CorruptMessage:
38                 return "Corrupt message";
39             case Status::MessageFormatUnknown:
40                 return "Unknown message format";
41             case Status::AllocationError:
42                 return "Allocation error";
43             case Status::CommunicationError:
44                 return "Communication error";
45             case Status::TooFrequentErrors:
46                 return "Too many errors (temporary lockout)";
47             case Status::BadRequest:
48                 return "HTTP 400/Bad request";
49             case Status::Unauthorized:
50                 return "HTTP 401/Unauthorized";
51             case Status::StreamingViolation:
52                 return "HTTP 429/Streaming Violation";
53             case Status::BadGateway:
54                 return "HTTP 502/Bad Gateway";
55             default:
56                 return "Error #" + std::to_string (int (status));
57         }
58     }
59 
60     /// Pandora login request
61     class AuthorizationRequest : public Request {
62         const std::string username;
63         const std::string password;
64         mutable std::string authorization_token;
65         mutable UserFeatures features;
66 
67     public:
AuthorizationRequest(const std::string & user,const std::string & pass)68         AuthorizationRequest (const std::string &user, const std::string &pass)
69         : Request (nullptr, "v1/auth/login"), username (user), password (pass){};
70 
retrieveRequestMessage() const71         virtual Parsnip::SerialData retrieveRequestMessage() const override {
72             return Parsnip::SerialData{Parsnip::SerialData::Dictionary,
73                                        "username",
74                                        username,
75                                        "password",
76                                        password,
77                                        "keepLoggedIn",
78                                        true};
79         }
extractResponse(const Parsnip::SerialData & message) const80         virtual void extractResponse (const Parsnip::SerialData &message) const override {
81             authorization_token = message["authToken"].asString();
82 
83             features.station_count = message["stationCount"].asInteger();
84 
85             auto &config = message["config"];
86             features.inactivity_timeout = config["inactivityTimeout"].asInteger();
87             features.daily_skip_limit = config["dailySkipLimit"].asInteger();
88             features.station_skip_limit = config["stationSkipLimit"].asInteger();
89 
90             for (const auto &flag : config["flags"]) {
91                 const std::string &f = flag.asString();
92                 if (f == "noAds") {
93                     features.adverts = false;
94                 } else if (f == "replaysEnabled") {
95                     features.replays = true;
96                 } else if (f == "highQualityStreamingAvailable") {
97                     features.hifi_audio_encoding = true;
98                 }
99             }
100         }
getResponse()101         inline const std::string &getResponse() {
102             return authorization_token;
103         }
getFeatures()104         inline const UserFeatures &getFeatures() {
105             return features;
106         }
107     };
108 
109     /// Construct a new communicator given the user's name and password, and an optional proxy server.
Communication(const std::string & name,const std::string & pass,const std::string & prox)110     Communication::Communication (const std::string &name, const std::string &pass, const std::string &prox)
111     : username (name), password (pass), proxy (prox) {
112     }
113 
114     /// Retrieve the Pandora home page and extract the CSRF token.
retrieveCSRFtoken()115     Status Communication::retrieveCSRFtoken() {
116         HttpClient::Request request;
117         request.type = HttpClient::RequestType::Head;
118         request.URL = "https://www.pandora.com";
119         request.proxy = proxy;
120         const HttpClient::Response response = http_client.performHttpRequest (request);
121         if (response.curl_code == CURLE_OK && response.http_status == 200) {
122             auto find = response.cookies.find ("csrftoken");
123             if (find != response.headers.end()) {
124                 csrf_token = find->second;
125                 flog (LOG_WHERE (LOG_PANDORA), "Pandora CSRF token retrieved.");
126                 return Status::Ok;
127             } else {
128                 flog (LOG_WHERE (LOG_ERROR), "No CSRF token.");
129                 response.dump();
130             }
131             return Status::MessageFormatUnknown;
132         }
133         flog (LOG_WHERE (LOG_ERROR), "HTTP request failed:");
134         response.dump();
135         return Status::CommunicationError;
136     }
137 
138     /** Perform an API request.
139         @param request The request to perform.
140         @return Status::Ok or an error value.
141         @throws Exceptions thrown by message decoders. */
performRequest(const Request & request)142     Status Communication::performRequest (const Request &request) {
143         try {
144             HttpClient::Request req;
145             req.type = HttpClient::RequestType::Post;
146             req.URL = EndpointUrl + request.endpoint;
147             req.proxy = proxy;
148             req.headers["X-CsrfToken"] = csrf_token;
149             req.headers["Content-Type"] = "application/json;charset=utf-8";
150             req.cookies["csrftoken"] = csrf_token;
151             if (!auth_token.empty()) {
152                 req.headers["X-AuthToken"] = auth_token;
153             }
154             req.debug = request.debug();
155             const auto request_message = request.retrieveRequestMessage();
156             req.body = request_message.toJson();
157 
158             // Set up for some logging controls
159             bool detail = (logging_enabled (LOG_PANDORA) && logging_enabled (LOG_PROTOCOL)) || req.debug;
160             LOG_TYPE PANDORA_HTTP = LOG_TYPE (detail ? 0 : (LOG_PANDORA | LOG_PROTOCOL));
161 
162             flog (LOG_WHERE (PANDORA_HTTP), "Pandora transaction to ", request.endpoint);
163             if (detail) {
164                 request_message.dumpJson ("Request");
165             }
166 
167             try {
168                 const HttpClient::Response response = http_client.performHttpRequest (req);
169                 if (response.http_status < 100 || response.http_status >= 300) {
170                     flog (LOG_WHERE (LOG_ERROR), "Failed HTTP request: ", request.endpoint);
171                     req.dump();
172                     response.dump();
173                     return Status (response.http_status);
174                 }
175                 Parsnip::SerialData response_message;
176                 try {
177                     response_message = Parsnip::SerialData::SerialData::parse_json (response.body);
178                     if (detail) {
179                         response_message.dumpJson ("Response");
180                     }
181                     request.extractResponse (response_message);
182                     return Status::Ok;
183                 } catch (const Parsnip::SerializationException &err) {
184                     flog (LOG_WHERE (LOG_ERROR), "Unexpected HTTP response: ", err.what());
185                     if (!detail) {
186                         request_message.dumpJson ("Request");
187                         response_message.dumpJson ("Response");
188                     }
189                     return Status::MessageFormatUnknown;
190                 }
191             } catch (const HttpClient::Exception &ex) {
192                 flog (LOG_WHERE (LOG_ERROR), ex.what());
193                 req.dump();
194                 if (!detail) {
195                     request_message.dumpJson ("Request");
196                 }
197                 return Status::CommunicationError;
198             }
199         } catch (const std::bad_alloc &err) {
200             flog (LOG_WHERE (LOG_ERROR), "Allocation error");
201             return Status::AllocationError;
202         }
203         assert (!"Unreachable");
204     }
205 
206     /// Authenticate with Pandora.
authenticate()207     Status Communication::authenticate() {
208         AuthorizationRequest request (username, password);
209         Status status = performRequest (request);
210         if (status == Status::Ok) {
211             auth_token = request.getResponse();
212             features = request.getFeatures();
213         }
214         return status;
215     }
216 
217     /** Execute an HTTP request.  Acquire CSRF token and authenticate if necessary.
218         - If an error indicates authentication has expired, log in again and retry request.
219         - For other errors, back off for a period to prevent overloading Pandora servers.
220         @param request The request to perform.
221         @internal @param retry_if_auth_required Internal use (for managing recursion).
222         @return Status::Ok, or an error. */
execute(Request & request,bool retry_if_auth_required)223     Status Communication::execute (Request &request, bool retry_if_auth_required) {
224         time_t now = time (nullptr);
225         if (lockout_until && (lockout_until > now)) {
226             return Status::TooFrequentErrors;
227         }
228         if (state == State::Authenticated && now >= session_expiration) {
229             state = State::Initialized;
230             auth_token.clear();
231         }
232         Status stat;
233         try {
234             switch (state) {
235                 case State::Uninitialized:
236                     stat = retrieveCSRFtoken();
237                     if (stat != Status::Ok) {
238                         flog (LOG_WHERE (LOG_PANDORA), "Pandora CSRF token retrieval failed.");
239                         break;
240                     }
241                     state = State::Initialized;
242                     // Fall through
243                 case State::Initialized:
244                     stat = authenticate();
245                     if (stat != Status::Ok) {
246                         flog (LOG_WHERE (LOG_PANDORA), "Pandora authentication failed.");
247                         break;
248                     }
249                     state = State::Authenticated;
250                     // Fall through
251                 case State::Authenticated:
252                     stat = performRequest (request);
253                     if (stat == Status::Unauthorized) {
254                         state = State::Initialized;
255                         auth_token.clear();
256                         if (retry_if_auth_required) {
257                             return execute (request, false);
258                         }
259                     }
260                     // Fall through
261             }
262         } catch (const HttpClient::Exception &ex) {
263             flog (LOG_ERROR, "HttpClient (", request.endpoint, "): ", ex.what());
264             stat = Status::CommunicationError;
265         }
266         if (stat != Status::Ok) {
267             // Gradually back off if we start getting errors.
268             if (sequential_failures < 8) {
269                 sequential_failures++;
270             }
271             if (sequential_failures > 2) {
272                 int duration = (1 << (sequential_failures - 1));
273                 flog (LOG_WHERE (LOG_ERROR),
274                       "Multiple successive failures, disabling communication for ",
275                       duration,
276                       " seconds");
277                 lockout_until = time (nullptr) + duration;
278             }
279         } else {
280             sequential_failures = 0;
281             lockout_until = 0;
282             session_expiration = now + features.inactivity_timeout;
283         }
284         return stat;
285     }
286 
287     /** Send a simple notification by hitting a URL. */
sendSimpleNotification(const std::string & url)288     Status Communication::sendSimpleNotification (const std::string &url) {
289         try {
290             HttpClient::Request req;
291             req.type = HttpClient::RequestType::Get;
292             req.URL = url;
293             req.proxy = proxy;
294             // If the site isn't Pandora, don't give them cookies or tokens.
295             int site_end = 0;
296             for (int slash_count = 3; slash_count > 0 && site_end < url.size(); site_end++) {
297                 if (url [site_end] == '/') {
298                     slash_count --;
299                 }
300             }
301             if (site_end > 12) {
302                 std::string site = url.substr (site_end - 12, 12);
303                 for (auto &ch : site) {
304                     ch = tolower (ch);
305                 }
306                 if (site == "pandora.com/") {
307                     req.cookies["csrftoken"] = csrf_token;
308                     if (!auth_token.empty()) {
309                         req.headers["X-AuthToken"] = auth_token;
310                     }
311                 }
312             }
313 
314             // This method is used for tracking notifications.  Many aren't
315             // even pandora.com.  So screw auth and CSRF tokens, there's enough
316             // tracking junk in their URLs.
317 
318             // Set up for some logging controls
319             LOG_TYPE PANDORA_HTTP = LOG_TYPE (LOG_PANDORA | LOG_PROTOCOL);
320             flog (LOG_WHERE (PANDORA_HTTP), "Pandora notification to ", url);
321 
322             try {
323                 const HttpClient::Response response = http_client.performHttpRequest (req);
324                 if (response.http_status < 100 || response.http_status >= 300) {
325                     flog (LOG_WHERE (LOG_ERROR), "Failed HTTP notification: ", url);
326                     req.dump();
327                     return Status (response.http_status);
328                 }
329                 return Status::Ok;
330             } catch (const HttpClient::Exception &ex) {
331                 flog (LOG_WHERE (LOG_ERROR), ex.what());
332                 req.dump();
333                 return Status::CommunicationError;
334             }
335         } catch (const std::bad_alloc &err) {
336             flog (LOG_WHERE (LOG_ERROR), "Allocation error");
337             return Status::AllocationError;
338         }
339         assert (!"Unreachable");
340     }
341 
342     /** Persist communication settings.
343         @return A dictionary with any settings the communicator wants saved. */
persist() const344     Parsnip::SerialData Communication::persist() const {
345         return Parsnip::SerialData{Parsnip::SerialData::Dictionary, Key::CSRFToken, csrf_token};
346     }
347 
348     /** Restore communication settings.
349         @param data A dictionary of previous settings from which to restore. */
restore(const Parsnip::SerialData & data)350     void Communication::restore (const Parsnip::SerialData &data) {
351         csrf_token = data [Key::CSRFToken].asString();
352         if (!csrf_token.empty()) {
353             state = State::Initialized;
354         }
355     }
356 
357 }  // namespace Pandora
358