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