move to new cspot

This commit is contained in:
philippe44
2023-03-25 16:48:41 -07:00
parent c712b78931
commit 008c36facf
2983 changed files with 465270 additions and 13569 deletions

View File

@@ -0,0 +1,53 @@
#include "AccessKeyFetcher.h"
#include <cstring>
#include "Logger.h"
#include "Utils.h"
using namespace cspot;
AccessKeyFetcher::AccessKeyFetcher(std::shared_ptr<cspot::Context> ctx) {
this->ctx = ctx;
}
AccessKeyFetcher::~AccessKeyFetcher() {}
bool AccessKeyFetcher::isExpired() {
if (accessKey.empty()) {
return true;
}
if (ctx->timeProvider->getSyncedTimestamp() > expiresAt) {
return true;
}
return false;
}
void AccessKeyFetcher::getAccessKey(AccessKeyFetcher::Callback callback) {
if (!isExpired()) {
return callback(accessKey);
}
CSPOT_LOG(info, "Access token expired, fetching new one...");
std::string url =
string_format("hm://keymaster/token/authenticated?client_id=%s&scope=%s",
CLIENT_ID.c_str(), SCOPES.c_str());
auto timeProvider = this->ctx->timeProvider;
ctx->session->execute(
MercurySession::RequestType::GET, url,
[this, timeProvider, callback](MercurySession::Response& res) {
if (res.fail) return;
char* accessKeyJson = (char*)res.parts[0].data();
auto accessJSON = std::string(accessKeyJson, strrchr(accessKeyJson, '}') - accessKeyJson + 1);
auto jsonBody = nlohmann::json::parse(accessJSON);
this->accessKey = jsonBody["accessToken"];
int expiresIn = jsonBody["expiresIn"];
expiresIn = expiresIn / 2; // Refresh token before it expires
this->expiresAt =
timeProvider->getSyncedTimestamp() + (expiresIn * 1000);
callback(jsonBody["accessToken"]);
});
}

View File

@@ -1,113 +1,23 @@
#include "ApResolve.h"
#include <memory>
#include <vector>
#include <string>
#include <iostream>
#include <ctype.h>
#include <cstring>
#include <stdlib.h>
#include <sys/types.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include "win32shim.h"
#else
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
#include <sstream>
#include <fstream>
#include "Logger.h"
#include <cJSON.h>
#include <ConfigJSON.h>
#include <random>
ApResolve::ApResolve() {}
using namespace cspot;
std::string ApResolve::getApList()
{
// hostname lookup
struct hostent *host = gethostbyname("apresolve.spotify.com");
struct sockaddr_in client;
if ((host == NULL) || (host->h_addr == NULL))
{
CSPOT_LOG(error, "apresolve: DNS lookup error");
throw std::runtime_error("Resolve failed");
}
// Prepare socket
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(80);
memcpy(&client.sin_addr, host->h_addr, host->h_length);
int sockFd = socket(AF_INET, SOCK_STREAM, 0);
// Connect to spotify's server
if (connect(sockFd, (struct sockaddr *)&client, sizeof(client)) < 0)
{
close(sockFd);
CSPOT_LOG(error, "Could not connect to apresolve");
throw std::runtime_error("Resolve failed");
}
// Prepare HTTP get header
std::stringstream ss;
ss << "GET / HTTP/1.1\r\n"
<< "Host: apresolve.spotify.com\r\n"
<< "Accept: application/json\r\n"
<< "Connection: close\r\n"
<< "\r\n\r\n";
std::string request = ss.str();
// Send the request
if (send(sockFd, request.c_str(), request.length(), 0) != (int)request.length())
{
CSPOT_LOG(error, "apresolve: can't send request");
throw std::runtime_error("Resolve failed");
}
char cur;
// skip read till json data
while (read(sockFd, &cur, 1) > 0 && cur != '{');
auto jsonData = std::string("{");
// Read json structure
while (read(sockFd, &cur, 1) > 0)
{
jsonData += cur;
}
close(sockFd);
return jsonData;
ApResolve::ApResolve(std::string apOverride)
{
this->apOverride = apOverride;
}
std::string ApResolve::fetchFirstApAddress()
{
if (configMan->apOverride != "")
if (apOverride != "")
{
return configMan->apOverride;
return apOverride;
}
// Fetch json body
auto jsonData = getApList();
auto request = bell::HTTPClient::get("https://apresolve.spotify.com/");
std::string_view responseStr = request->body();
// Use cJSON to get first ap address
auto root = cJSON_Parse(jsonData.c_str());
auto apList = cJSON_GetObjectItemCaseSensitive(root, "ap_list");
auto firstAp = cJSON_GetArrayItem(apList, 0);
auto data = std::string(firstAp->valuestring);
// release cjson memory
cJSON_Delete(root);
return data;
// parse json with nlohmann
auto json = nlohmann::json::parse(responseStr);
return json["ap_list"][0];
}

View File

@@ -1,57 +0,0 @@
#include "AudioChunk.h"
std::vector<uint8_t> audioAESIV({0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93});
AudioChunk::AudioChunk(uint16_t seqId, std::vector<uint8_t> &audioKey, uint32_t startPosition, uint32_t predictedEndPosition)
{
this->crypto = std::make_unique<Crypto>();
this->seqId = seqId;
this->audioKey = audioKey;
this->startPosition = startPosition;
this->endPosition = predictedEndPosition;
this->decryptedData = std::vector<uint8_t>();
this->isHeaderFileSizeLoadedSemaphore = std::make_unique<WrappedSemaphore>(5);
this->isLoadedSemaphore = std::make_unique<WrappedSemaphore>(5);
}
AudioChunk::~AudioChunk()
{
}
void AudioChunk::appendData(const std::vector<uint8_t> &data)
{
//if (this == nullptr) return;
this->decryptedData.insert(this->decryptedData.end(), data.begin(), data.end());
}
void AudioChunk::readData(uint8_t *target, size_t offset, size_t nbytes) {
auto readPos = offset + nbytes;
auto modulo = (readPos % 16);
auto ivReadPos = readPos;
if (modulo != 0) {
ivReadPos += (16 - modulo);
}
if (ivReadPos > decryptedCount) {
// calculate the IV for right position
auto calculatedIV = this->getIVSum((oldStartPos + decryptedCount) / 16);
crypto->aesCTRXcrypt(this->audioKey, calculatedIV, decryptedData.data() + decryptedCount, ivReadPos - decryptedCount);
decryptedCount = ivReadPos;
}
memcpy(target, this->decryptedData.data() + offset, nbytes);
}
void AudioChunk::finalize()
{
this->oldStartPos = this->startPosition;
this->startPosition = this->endPosition - this->decryptedData.size();
this->isLoaded = true;
}
// Basically just big num addition
std::vector<uint8_t> AudioChunk::getIVSum(uint32_t n)
{
return bigNumAdd(audioAESIV, n);
}

View File

@@ -1,116 +0,0 @@
#include "AudioChunkManager.h"
#include "BellUtils.h"
#include "Logger.h"
AudioChunkManager::AudioChunkManager()
: bell::Task("AudioChunkManager", 4 * 1024, 0, 1) {
this->chunks = std::vector<std::shared_ptr<AudioChunk>>();
startTask();
}
std::shared_ptr<AudioChunk>
AudioChunkManager::registerNewChunk(uint16_t seqId,
std::vector<uint8_t> &audioKey,
uint32_t startPos, uint32_t endPos) {
std::scoped_lock lock(chunkMutex);
auto chunk =
std::make_shared<AudioChunk>(seqId, audioKey, startPos * 4, endPos * 4);
this->chunks.push_back(chunk);
CSPOT_LOG(debug, "Chunk requested %d", seqId);
return chunk;
}
void AudioChunkManager::handleChunkData(std::vector<uint8_t> &data,
bool failed) {
auto audioPair = std::pair(data, failed);
audioChunkDataQueue.push(audioPair);
}
void AudioChunkManager::failAllChunks() {
std::scoped_lock lock(chunkMutex);
// Enumerate all the chunks and mark em all failed
for (auto const &chunk : this->chunks) {
if (!chunk->isLoaded) {
chunk->isLoaded = true;
chunk->isFailed = true;
chunk->isHeaderFileSizeLoadedSemaphore->give();
chunk->isLoadedSemaphore->give();
}
}
}
void AudioChunkManager::close() {
this->isRunning = false;
this->failAllChunks();
this->audioChunkDataQueue.clear();
std::scoped_lock lock(this->runningMutex);
}
void AudioChunkManager::runTask() {
std::scoped_lock lock(this->runningMutex);
this->isRunning = true;
std::pair<std::vector<uint8_t>, bool> audioPair;
while (isRunning) {
if (this->audioChunkDataQueue.wtpop(audioPair, 100)) {
std::scoped_lock lock(this->chunkMutex);
auto data = audioPair.first;
auto failed = audioPair.second;
uint16_t seqId = ntohs(extract<uint16_t>(data, 0));
// Erase all chunks that are not referenced elsewhere anymore
chunks.erase(
std::remove_if(chunks.begin(), chunks.end(),
[](std::shared_ptr<AudioChunk>& chunk) {
return chunk.use_count() == 1;
}),
chunks.end());
try {
for (auto const &chunk : this->chunks) {
// Found the right chunk
if (chunk != nullptr && chunk->seqId == seqId) {
if (failed) {
chunk->isFailed = true;
chunk->startPosition = 0;
chunk->endPosition = 0;
chunk->isHeaderFileSizeLoadedSemaphore->give();
chunk->isLoadedSemaphore->give();
break;
}
switch (data.size()) {
case DATA_SIZE_HEADER: {
CSPOT_LOG(debug, "ID: %d: header finalize!", seqId);
auto headerSize = ntohs(extract<uint16_t>(data, 2));
// Got file size!
chunk->headerFileSize =
ntohl(extract<uint32_t>(data, 5)) * 4;
chunk->isHeaderFileSizeLoadedSemaphore->give();
break;
}
case DATA_SIZE_FOOTER:
if (chunk->endPosition > chunk->headerFileSize) {
chunk->endPosition = chunk->headerFileSize;
}
CSPOT_LOG(debug, "ID: %d: finalize chunk!",
seqId);
chunk->finalize();
chunk->isLoadedSemaphore->give();
break;
default:
auto actualData = std::vector<uint8_t>(
data.begin() + 2, data.end());
chunk->appendData(actualData);
break;
}
}
}
} catch (...) {
}
}
}
// Playback finished
}

View File

@@ -0,0 +1,121 @@
#include "AuthChallenges.h"
using namespace cspot;
using random_bytes_engine =
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
AuthChallenges::AuthChallenges() {
this->crypto = std::make_unique<Crypto>();
this->clientHello = {};
this->apResponse = {};
this->authRequest = {};
this->clientResPlaintext = {};
}
AuthChallenges::~AuthChallenges() {
// Destruct the protobufs
pb_release(ClientHello_fields, &clientHello);
pb_release(APResponseMessage_fields, &apResponse);
pb_release(ClientResponsePlaintext_fields, &clientResPlaintext);
pb_release(ClientResponseEncrypted_fields, &authRequest);
}
std::vector<uint8_t> AuthChallenges::prepareAuthPacket(
std::vector<uint8_t>& authData, int authType, const std::string& deviceId,
const std::string& username) {
// prepare authentication request proto
pbPutString(username, authRequest.login_credentials.username);
std::copy(authData.begin(), authData.end(),
authRequest.login_credentials.auth_data.bytes);
authRequest.login_credentials.auth_data.size = authData.size();
authRequest.login_credentials.typ = (AuthenticationType)authType;
authRequest.system_info.cpu_family = CpuFamily_CPU_UNKNOWN;
authRequest.system_info.os = Os_OS_UNKNOWN;
auto infoStr = std::string("cspot-player");
pbPutString(infoStr, authRequest.system_info.system_information_string);
pbPutString(deviceId, authRequest.system_info.device_id);
auto versionStr = std::string("cspot-1.1");
pbPutString(versionStr, authRequest.version_string);
authRequest.has_version_string = true;
return pbEncode(ClientResponseEncrypted_fields, &authRequest);
}
std::vector<uint8_t> AuthChallenges::solveApHello(
std::vector<uint8_t>& helloPacket, std::vector<uint8_t>& data) {
// Decode the response
auto skipSize = std::vector<uint8_t>(data.begin() + 4, data.end());
pb_release(APResponseMessage_fields, &apResponse);
pbDecode(apResponse, APResponseMessage_fields, skipSize);
auto diffieKey = std::vector<uint8_t>(
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs,
apResponse.challenge.login_crypto_challenge.diffie_hellman.gs + 96);
// Compute the diffie hellman shared key based on the response
auto sharedKey = this->crypto->dhCalculateShared(diffieKey);
// Init client packet + Init server packets are required for the hmac challenge
data.insert(data.begin(), helloPacket.begin(), helloPacket.end());
// Solve the hmac challenge
auto resultData = std::vector<uint8_t>(0);
for (int x = 1; x < 6; x++) {
auto challengeVector = std::vector<uint8_t>(1);
challengeVector[0] = x;
challengeVector.insert(challengeVector.begin(), data.begin(), data.end());
auto digest = crypto->sha1HMAC(sharedKey, challengeVector);
resultData.insert(resultData.end(), digest.begin(), digest.end());
}
auto lastVec =
std::vector<uint8_t>(resultData.begin(), resultData.begin() + 0x14);
// Digest generated!
auto digest = crypto->sha1HMAC(lastVec, data);
clientResPlaintext.login_crypto_response.has_diffie_hellman = true;
std::copy(digest.begin(), digest.end(),
clientResPlaintext.login_crypto_response.diffie_hellman.hmac);
// Get send and receive keys
this->shanSendKey = std::vector<uint8_t>(resultData.begin() + 0x14,
resultData.begin() + 0x34);
this->shanRecvKey = std::vector<uint8_t>(resultData.begin() + 0x34,
resultData.begin() + 0x54);
return pbEncode(ClientResponsePlaintext_fields, &clientResPlaintext);
}
std::vector<uint8_t> AuthChallenges::prepareClientHello() {
// Prepare protobuf message
this->crypto->dhInit();
// Copy the public key into diffiehellman hello packet
std::copy(this->crypto->publicKey.begin(), this->crypto->publicKey.end(),
clientHello.login_crypto_hello.diffie_hellman.gc);
clientHello.login_crypto_hello.diffie_hellman.server_keys_known = 1;
clientHello.build_info.product = Product_PRODUCT_CLIENT;
clientHello.build_info.platform = Platform2_PLATFORM_LINUX_X86;
clientHello.build_info.version = SPOTIFY_VERSION;
clientHello.feature_set.autoupdate2 = true;
clientHello.cryptosuites_supported[0] = Cryptosuite_CRYPTO_SUITE_SHANNON;
clientHello.padding[0] = 0x1E;
clientHello.has_feature_set = true;
clientHello.login_crypto_hello.has_diffie_hellman = true;
clientHello.has_padding = true;
clientHello.has_feature_set = true;
// Generate the random nonce
auto nonce = crypto->generateVectorWithRandomData(16);
std::copy(nonce.begin(), nonce.end(), clientHello.client_nonce);
return pbEncode(ClientHello_fields, &clientHello);
}

View File

@@ -0,0 +1,182 @@
#include "CDNTrackStream.h"
using namespace cspot;
CDNTrackStream::CDNTrackStream(
std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher) {
this->accessKeyFetcher = accessKeyFetcher;
this->status = Status::INITIALIZING;
this->trackReady = std::make_unique<bell::WrappedSemaphore>(5);
this->crypto = std::make_unique<Crypto>();
}
CDNTrackStream::~CDNTrackStream() {
}
void CDNTrackStream::fail() {
this->status = Status::FAILED;
this->trackReady->give();
}
void CDNTrackStream::fetchFile(const std::vector<uint8_t>& trackId,
const std::vector<uint8_t>& audioKey) {
this->status = Status::HAS_DATA;
this->trackId = trackId;
this->audioKey = std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
accessKeyFetcher->getAccessKey([this, trackId, audioKey](std::string key) {
CSPOT_LOG(info, "Received access key, fetching CDN URL...");
std::string requestUrl = string_format(
"https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
"%s?alt=json&product=9",
bytesToHexString(trackId).c_str());
auto req = bell::HTTPClient::get(
requestUrl,
{bell::HTTPClient::ValueHeader({"Authorization", "Bearer " + key})});
std::string_view result = req->body();
auto jsonResult = nlohmann::json::parse(result);
std::string cdnUrl = jsonResult["cdnurl"][0];
if (this->status != Status::FAILED) {
this->cdnUrl = cdnUrl;
this->status = Status::HAS_URL;
CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
this->openStream();
this->trackReady->give();
}
});
}
size_t CDNTrackStream::getPosition() {
return this->position;
}
void CDNTrackStream::seek(size_t newPos) {
this->enableRequestMargin = true;
this->position = newPos;
}
void CDNTrackStream::openStream() {
CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
// Open connection, read first 128 bytes
this->httpConnection = bell::HTTPClient::get(
this->cdnUrl,
{bell::HTTPClient::RangeHeader::range(0, OPUS_HEADER_SIZE - 1)});
this->httpConnection->stream().read((char*)header.data(), OPUS_HEADER_SIZE);
this->totalFileSize =
this->httpConnection->totalLength() - SPOTIFY_OPUS_HEADER;
this->decrypt(header.data(), OPUS_HEADER_SIZE, 0);
// Location must be dividable by 16
size_t footerStartLocation =
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) -
(this->totalFileSize - OPUS_FOOTER_PREFFERED + SPOTIFY_OPUS_HEADER) % 16;
this->footer = std::vector<uint8_t>(
this->totalFileSize - footerStartLocation + SPOTIFY_OPUS_HEADER);
this->httpConnection->get(
cdnUrl, {bell::HTTPClient::RangeHeader::last(footer.size())});
this->httpConnection->stream().read((char*)footer.data(),
this->footer.size());
this->decrypt(footer.data(), footer.size(), footerStartLocation);
CSPOT_LOG(info, "Header and footer bytes received");
this->position = 0;
this->lastRequestPosition = 0;
this->lastRequestCapacity = 0;
this->isConnected = true;
}
size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
size_t offsetPosition = position + SPOTIFY_OPUS_HEADER;
size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER;
if (position + bytes >= this->totalFileSize) {
return 0;
}
// // Opus tries to read header, use prefetched data
if (offsetPosition < OPUS_HEADER_SIZE &&
bytes + offsetPosition <= OPUS_HEADER_SIZE) {
memcpy(dst, this->header.data() + offsetPosition, bytes);
position += bytes;
return bytes;
}
// // Opus tries to read footer, use prefetched data
if (offsetPosition >= (actualFileSize - this->footer.size())) {
size_t toReadBytes = bytes;
if ((position + bytes) > this->totalFileSize) {
// Tries to read outside of bounds, truncate
toReadBytes = this->totalFileSize - position;
}
size_t footerOffset =
offsetPosition - (actualFileSize - this->footer.size());
memcpy(dst, this->footer.data() + footerOffset, toReadBytes);
position += toReadBytes;
return toReadBytes;
}
// Data not in the headers. Make sense of whats going on.
// Position in bounds :)
if (offsetPosition >= this->lastRequestPosition &&
offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) {
size_t toRead = bytes;
if ((toRead + offsetPosition) >
this->lastRequestPosition + lastRequestCapacity) {
toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition;
}
memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition,
toRead);
position += toRead;
return toRead;
} else {
size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16);
if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) {
requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) -
((offsetPosition - SEEK_MARGIN_SIZE) % 16);
this->enableRequestMargin = false;
}
this->httpConnection->get(
cdnUrl, {bell::HTTPClient::RangeHeader::range(
requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)});
this->lastRequestPosition = requestPosition;
this->lastRequestCapacity = this->httpConnection->contentLength();
this->httpConnection->stream().read((char*)this->httpBuffer.data(),
lastRequestCapacity);
this->decrypt(this->httpBuffer.data(), lastRequestCapacity,
this->lastRequestPosition);
return readBytes(dst, bytes);
}
return bytes;
}
size_t CDNTrackStream::getSize() {
return this->totalFileSize;
}
void CDNTrackStream::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
auto calculatedIV = bigNumAdd(audioAESIV, pos / 16);
this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes);
}

View File

@@ -1,176 +0,0 @@
#include "ChunkedAudioStream.h"
#include "Logger.h"
#include "BellUtils.h"
static size_t vorbisReadCb(void *ptr, size_t size, size_t nmemb, ChunkedAudioStream *self)
{
size_t readSize = 0;
while (readSize < nmemb * size && self->byteStream->position() < self->byteStream->size() && self->isRunning) {
size_t bytes = self->byteStream->read((uint8_t *) ptr + readSize, (size * nmemb) - readSize);
if (bytes <= 0) {
CSPOT_LOG(info, "unexpected end/error of stream");
return readSize;
}
readSize += bytes;
}
return readSize;
}
static int vorbisCloseCb(ChunkedAudioStream *self)
{
return 0;
}
static int vorbisSeekCb(ChunkedAudioStream *self, int64_t offset, int whence)
{
if (whence == 0)
{
offset += SPOTIFY_HEADER_SIZE;
}
static constexpr std::array<Whence, 3> seekDirections{
Whence::START, Whence::CURRENT, Whence::END};
self->seek(offset, seekDirections.at(static_cast<size_t>(whence)));
return 0;
}
static long vorbisTellCb(ChunkedAudioStream *self)
{
return static_cast<long>(self->byteStream->position());
}
ChunkedAudioStream::~ChunkedAudioStream()
{
}
ChunkedAudioStream::ChunkedAudioStream(std::vector<uint8_t> fileId, std::vector<uint8_t> audioKey, uint32_t duration, std::shared_ptr<MercuryManager> manager, uint32_t startPositionMs, bool isPaused)
{
this->duration = duration;
this->startPositionMs = startPositionMs;
this->isPaused = isPaused;
// auto beginChunk = manager->fetchAudioChunk(fileId, audioKey, 0, 0x4000);
// beginChunk->keepInMemory = true;
// while(beginChunk->isHeaderFileSizeLoadedSemaphore->twait() != 0);
// this->fileSize = beginChunk->headerFileSize;
// chunks.push_back(beginChunk);
//
// // File size is required for this packet to be downloaded
// this->fetchTraillingPacket();
this->byteStream = std::make_shared<ChunkedByteStream>(manager);
this->byteStream->setFileInfo(fileId, audioKey);
this->byteStream->fetchFileInformation();
vorbisFile = { };
vorbisCallbacks =
{
(decltype(ov_callbacks::read_func)) & vorbisReadCb,
(decltype(ov_callbacks::seek_func)) & vorbisSeekCb,
(decltype(ov_callbacks::close_func)) & vorbisCloseCb,
(decltype(ov_callbacks::tell_func)) & vorbisTellCb,
};
}
void ChunkedAudioStream::seekMs(uint32_t positionMs)
{
byteStream->setEnableLoadAhead(false);
this->seekMutex.lock();
ov_time_seek(&vorbisFile, positionMs);
this->seekMutex.unlock();
byteStream->setEnableLoadAhead(true);
CSPOT_LOG(debug, "--- Finished seeking!");
}
void ChunkedAudioStream::startPlaybackLoop(uint8_t *pcmOut, size_t pcmOut_len)
{
isRunning = true;
byteStream->setEnableLoadAhead(false);
int32_t r = ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks);
CSPOT_LOG(debug, "--- Loaded file");
if (this->startPositionMs != 0)
{
ov_time_seek(&vorbisFile, startPositionMs);
}
bool eof = false;
byteStream->setEnableLoadAhead(true);
while (!eof && isRunning)
{
if (!isPaused)
{
this->seekMutex.lock();
long ret = ov_read(&vorbisFile, (char *)&pcmOut[0], pcmOut_len, &currentSection);
this->seekMutex.unlock();
if (ret == 0)
{
CSPOT_LOG(info, "EOL");
// and done :)
eof = true;
}
else if (ret < 0)
{
CSPOT_LOG(error, "An error has occured in the stream");
// Error in the stream
}
else
{
// Write the actual data
pcmCallback(pcmOut, ret);
// audioSink->feedPCMFrames(data);
}
}
else
{
BELL_SLEEP_MS(100);
}
}
ov_clear(&vorbisFile);
vorbisCallbacks = {};
CSPOT_LOG(debug, "Track finished");
finished = true;
if (eof)
{
this->streamFinishedCallback();
}
}
//
//void ChunkedAudioStream::fetchTraillingPacket()
//{
// auto startPosition = (this->fileSize / 4) - 0x1000;
//
// // AES block size is 16, so the index must be divisible by it
// while ((startPosition * 4) % 16 != 0)
// startPosition++; // ik, ugly lol
//
// auto endChunk = manager->fetchAudioChunk(fileId, audioKey, startPosition, fileSize / 4);
// endChunk->keepInMemory = true;
//
// chunks.push_back(endChunk);
// while (endChunk->isLoadedSemaphore->twait() != 0);
//}
void ChunkedAudioStream::seek(size_t dpos, Whence whence)
{
auto seekPos = 0;
switch (whence)
{
case Whence::START:
seekPos = dpos;
break;
case Whence::CURRENT:
seekPos = byteStream->position() + dpos;
break;
case Whence::END:
seekPos = byteStream->size() + dpos;
break;
}
byteStream->seek(seekPos);
}

View File

@@ -1,155 +0,0 @@
#include "ChunkedByteStream.h"
ChunkedByteStream::ChunkedByteStream(std::shared_ptr<MercuryManager> manager) {
this->mercuryManager = manager;
this->pos = 167; // spotify header size
}
void ChunkedByteStream::setFileInfo(std::vector<uint8_t> &fileId, std::vector<uint8_t> &audioKey) {
this->audioKey = audioKey;
this->fileId = fileId;
}
void ChunkedByteStream::setEnableLoadAhead(bool loadAhead) {
this->loadAheadEnabled = loadAhead;
}
void ChunkedByteStream::fetchFileInformation() {
std::shared_ptr<AudioChunk> beginChunk = mercuryManager->fetchAudioChunk(fileId, audioKey, 0, 0x4000);
beginChunk->keepInMemory = true;
while (beginChunk->isHeaderFileSizeLoadedSemaphore->twait() != 0);
this->fileSize = beginChunk->headerFileSize;
chunks.push_back(beginChunk);
auto startPosition = (this->fileSize / 4) - 0x1000;
// AES block size is 16, so the index must be divisible by it
while ((startPosition * 4) % 16 != 0)
startPosition++; // ik, ugly lol
auto endChunk = mercuryManager->fetchAudioChunk(fileId, audioKey, startPosition, fileSize / 4);
endChunk->keepInMemory = true;
chunks.push_back(endChunk);
requestChunk(0);
}
std::shared_ptr<AudioChunk> ChunkedByteStream::getChunkForPosition(size_t position) {
// Find chunks that fit in requested position
for (auto chunk: chunks) {
if (chunk->startPosition <= position && chunk->endPosition > position) {
return chunk;
}
}
return nullptr;
}
std::shared_ptr<AudioChunk> ChunkedByteStream::requestChunk(uint16_t position) {
auto chunk = this->mercuryManager->fetchAudioChunk(this->fileId, this->audioKey, position);
// Store a reference internally
chunks.push_back(chunk);
return chunk;
}
size_t ChunkedByteStream::read(uint8_t *buf, size_t nbytes) {
std::scoped_lock lock(this->readMutex);
auto chunk = getChunkForPosition(pos);
uint16_t chunkIndex = this->pos / AUDIO_CHUNK_SIZE;
if (loadAheadEnabled) {
for (auto it = chunks.begin(); it != chunks.end();) {
if (((*it)->endPosition<pos || (*it)->startPosition>(pos + 2 * AUDIO_CHUNK_SIZE)) && !(*it)->keepInMemory) {
it = chunks.erase(it);
} else {
it++;
}
}
}
// Request chunk if does not exist
if (chunk == nullptr) {
BELL_LOG(info, "cspot", "Chunk not found, requesting %d", chunkIndex);
chunk = this->requestChunk(chunkIndex);
}
if (chunk != nullptr) {
// Wait for chunk if not loaded yet
if (!chunk->isLoaded && !chunk->isFailed) {
BELL_LOG(info, "cspot", "Chunk not loaded, waiting for %d", chunkIndex);
chunk->isLoadedSemaphore->wait();
}
if (chunk->isFailed) return 0;
// Attempt to read from chunk
auto read = attemptRead(buf, nbytes, chunk);
pos += read;
auto nextChunkPos = ((chunkIndex + 1) * AUDIO_CHUNK_SIZE) + 1;
if (loadAheadEnabled && nextChunkPos < fileSize) {
auto nextChunk = getChunkForPosition(nextChunkPos);
if (nextChunk == nullptr) {
// Request next chunk
this->requestChunk(chunkIndex + 1);
}
}
return read;
}
return 0;
}
size_t ChunkedByteStream::attemptRead(uint8_t *buffer, size_t bytes, std::shared_ptr<AudioChunk> chunk) {
//if (!chunk->isLoaded || chunk->isFailed || chunk->startPosition >= pos || chunk->endPosition < pos) return 0;
// Calculate how many bytes we can read from chunk
auto offset = pos - chunk->startPosition;
auto toRead = bytes;
if (toRead > chunk->decryptedData.size() - offset) {
toRead = chunk->decryptedData.size() - offset;
}
chunk->readData(buffer, offset, toRead);
return toRead;
}
void ChunkedByteStream::seek(size_t nbytes) {
std::scoped_lock lock(this->readMutex);
pos = nbytes;
if (getChunkForPosition(this->pos) == nullptr) {
// Seeking might look back - therefore we preload some past data
auto startPosition = (this->pos / 4) - (AUDIO_CHUNK_SIZE / 4);
// AES block size is 16, so the index must be divisible by it
while ((startPosition * 4) % 16 != 0)
startPosition++; // ik, ugly lol
this->chunks.push_back(mercuryManager->fetchAudioChunk(fileId, audioKey, startPosition,
startPosition + (AUDIO_CHUNK_SIZE / 4)));
}
}
size_t ChunkedByteStream::skip(size_t nbytes) {
std::scoped_lock lock(this->readMutex);
pos += nbytes;
return pos;
}
size_t ChunkedByteStream::position() {
return pos;
}
size_t ChunkedByteStream::size() {
return fileSize;
}
void ChunkedByteStream::close() {
}

View File

@@ -1,94 +0,0 @@
#include "ConfigJSON.h"
#include "JSONObject.h"
#include "Logger.h"
#include "ConstantParameters.h"
ConfigJSON::ConfigJSON(std::string jsonFileName, std::shared_ptr<FileHelper> file)
{
_file = file;
_jsonFileName = jsonFileName;
}
bool ConfigJSON::load()
{
// Config filename check
if(_jsonFileName.length() > 0)
{
std::string jsonConfig;
_file->readFile(_jsonFileName, jsonConfig);
// Ignore config if empty
if(jsonConfig.length() > 0)
{
auto root = cJSON_Parse(jsonConfig.c_str());
if(cJSON_HasObjectItem(root, "deviceName"))
{
auto deviceNameObject = cJSON_GetObjectItemCaseSensitive(root, "deviceName");
this->deviceName = std::string(cJSON_GetStringValue(deviceNameObject));
}
if(cJSON_HasObjectItem(root, "bitrate"))
{
auto bitrateObject = cJSON_GetObjectItemCaseSensitive(root, "bitrate");
switch((uint16_t)cJSON_GetNumberValue(bitrateObject)){
case 320:
this->format = AudioFormat_OGG_VORBIS_320;
break;
case 160:
this->format = AudioFormat_OGG_VORBIS_160;
break;
case 96:
this->format = AudioFormat_OGG_VORBIS_96;
break;
default:
this->format = AudioFormat_OGG_VORBIS_320;
break;
}
}
if(cJSON_HasObjectItem(root, "volume"))
{
auto volumeObject = cJSON_GetObjectItemCaseSensitive(root, "volume");
this->volume = cJSON_GetNumberValue(volumeObject);
}
cJSON_Delete(root);
}
else
{
// Config file not found or invalid
// Set default values
this->volume = 32767;
this->deviceName = defaultDeviceName;
this->format = AudioFormat_OGG_VORBIS_160;
}
return true;
}
else
{
return false;
}
}
bool ConfigJSON::save()
{
bell::JSONObject obj;
obj["volume"] = this->volume;
obj["deviceName"] = this->deviceName;
switch(this->format){
case AudioFormat_OGG_VORBIS_320:
obj["bitrate"] = 320;
break;
case AudioFormat_OGG_VORBIS_160:
obj["bitrate"] = 160;
break;
case AudioFormat_OGG_VORBIS_96:
obj["bitrate"] = 96;
break;
default:
obj["bitrate"] = 160;
break;
}
return _file->writeFile(_jsonFileName, obj.toString());
}

View File

@@ -1,133 +1,201 @@
#include "LoginBlob.h"
#include "JSONObject.h"
#include "ConstantParameters.h"
#include "Logger.h"
LoginBlob::LoginBlob()
{
this->crypto = std::make_unique<Crypto>();
using namespace cspot;
LoginBlob::LoginBlob(std::string name) {
char hash[32];
sprintf(hash, "%016zu", std::hash<std::string>{}(name));
// base is 142137fd329622137a14901634264e6f332e2411
this->deviceId = std::string("142137fd329622137a149016") + std::string(hash);
this->crypto = std::make_unique<Crypto>();
this->name = name;
this->crypto->dhInit();
}
std::vector<uint8_t> LoginBlob::decodeBlob(const std::vector<uint8_t> &blob, const std::vector<uint8_t> &sharedKey)
{
// 0:16 - iv; 17:-20 - blob; -20:0 - checksum
auto iv = std::vector<uint8_t>(blob.begin(), blob.begin() + 16);
auto encrypted = std::vector<uint8_t>(blob.begin() + 16, blob.end() - 20);
auto checksum = std::vector<uint8_t>(blob.end() - 20, blob.end());
std::vector<uint8_t> LoginBlob::decodeBlob(
const std::vector<uint8_t>& blob, const std::vector<uint8_t>& sharedKey) {
// 0:16 - iv; 17:-20 - blob; -20:0 - checksum
auto iv = std::vector<uint8_t>(blob.begin(), blob.begin() + 16);
auto encrypted = std::vector<uint8_t>(blob.begin() + 16, blob.end() - 20);
auto checksum = std::vector<uint8_t>(blob.end() - 20, blob.end());
// baseKey = sha1(sharedKey) 0:16
crypto->sha1Init();
crypto->sha1Update(sharedKey);
auto baseKey = crypto->sha1FinalBytes();
baseKey = std::vector<uint8_t>(baseKey.begin(), baseKey.begin() + 16);
// baseKey = sha1(sharedKey) 0:16
crypto->sha1Init();
auto checksumMessage = std::string("checksum");
auto checksumKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
crypto->sha1Update(sharedKey);
auto baseKey = crypto->sha1FinalBytes();
baseKey = std::vector<uint8_t>(baseKey.begin(), baseKey.begin() + 16);
auto encryptionMessage = std::string("encryption");
auto encryptionKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
auto checksumMessage = std::string("checksum");
auto checksumKey = crypto->sha1HMAC(
baseKey,
std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
auto encryptionMessage = std::string("encryption");
auto encryptionKey = crypto->sha1HMAC(
baseKey,
std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
// Check checksum
if (mac != checksum)
{
CSPOT_LOG(error, "Mac doesn't match!" );
}
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
encryptionKey = std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted.data(), encrypted.size());
// Check checksum
if (mac != checksum) {
CSPOT_LOG(error, "Mac doesn't match!");
}
return encrypted;
encryptionKey =
std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted.data(), encrypted.size());
return encrypted;
}
uint32_t LoginBlob::readBlobInt(const std::vector<uint8_t> &data)
{
auto lo = data[blobSkipPosition];
if ((int)(lo & 0x80) == 0)
{
this->blobSkipPosition += 1;
return lo;
}
uint32_t LoginBlob::readBlobInt(const std::vector<uint8_t>& data) {
auto lo = data[blobSkipPosition];
if ((int)(lo & 0x80) == 0) {
this->blobSkipPosition += 1;
return lo;
}
auto hi = data[blobSkipPosition + 1];
this->blobSkipPosition += 2;
auto hi = data[blobSkipPosition + 1];
this->blobSkipPosition += 2;
return (uint32_t)((lo & 0x7f) | (hi << 7));
return (uint32_t)((lo & 0x7f) | (hi << 7));
}
std::vector<uint8_t> LoginBlob::decodeBlobSecondary(const std::vector<uint8_t> &blob, const std::string &username, const std::string &deviceId)
{
auto encryptedString = std::string(blob.begin(), blob.end());
auto blobData = crypto->base64Decode(encryptedString);
std::vector<uint8_t> LoginBlob::decodeBlobSecondary(
const std::vector<uint8_t>& blob, const std::string& username,
const std::string& deviceId) {
auto encryptedString = std::string(blob.begin(), blob.end());
auto blobData = crypto->base64Decode(encryptedString);
crypto->sha1Init();
crypto->sha1Update(std::vector<uint8_t>(deviceId.begin(), deviceId.end()));
auto secret = crypto->sha1FinalBytes();
auto pkBaseKey = crypto->pbkdf2HmacSha1(secret, std::vector<uint8_t>(username.begin(), username.end()), 256, 20);
crypto->sha1Init();
crypto->sha1Update(std::vector<uint8_t>(deviceId.begin(), deviceId.end()));
auto secret = crypto->sha1FinalBytes();
auto pkBaseKey = crypto->pbkdf2HmacSha1(
secret, std::vector<uint8_t>(username.begin(), username.end()), 256, 20);
crypto->sha1Init();
crypto->sha1Update(pkBaseKey);
auto key = std::vector<uint8_t>({0x00, 0x00, 0x00, 0x14}); // len of base key
auto baseKeyHashed = crypto->sha1FinalBytes();
key.insert(key.begin(), baseKeyHashed.begin(), baseKeyHashed.end());
crypto->sha1Init();
crypto->sha1Update(pkBaseKey);
auto key = std::vector<uint8_t>({0x00, 0x00, 0x00, 0x14}); // len of base key
auto baseKeyHashed = crypto->sha1FinalBytes();
key.insert(key.begin(), baseKeyHashed.begin(), baseKeyHashed.end());
crypto->aesECBdecrypt(key, blobData);
crypto->aesECBdecrypt(key, blobData);
auto l = blobData.size();
auto l = blobData.size();
for (int i = 0; i < l - 16; i++)
{
blobData[l - i - 1] ^= blobData[l - i - 17];
}
for (int i = 0; i < l - 16; i++) {
blobData[l - i - 1] ^= blobData[l - i - 17];
}
return blobData;
return blobData;
}
void LoginBlob::loadZeroconf(const std::vector<uint8_t> &blob, const std::vector<uint8_t> &sharedKey, const std::string &deviceId, const std::string &username)
{
auto partDecoded = this->decodeBlob(blob, sharedKey);
auto loginData = this->decodeBlobSecondary(partDecoded, username, deviceId);
void LoginBlob::loadZeroconf(const std::vector<uint8_t>& blob,
const std::vector<uint8_t>& sharedKey,
const std::string& deviceId,
const std::string& username) {
// Parse blob
blobSkipPosition = 1;
blobSkipPosition += readBlobInt(loginData);
blobSkipPosition += 1;
this->authType = readBlobInt(loginData);
blobSkipPosition += 1;
auto authSize = readBlobInt(loginData);
this->username = username;
this->authData = std::vector<uint8_t>(loginData.begin() + blobSkipPosition, loginData.begin() + blobSkipPosition + authSize);
auto partDecoded = this->decodeBlob(blob, sharedKey);
auto loginData = this->decodeBlobSecondary(partDecoded, username, deviceId);
// Parse blob
blobSkipPosition = 1;
blobSkipPosition += readBlobInt(loginData);
blobSkipPosition += 1;
this->authType = readBlobInt(loginData);
blobSkipPosition += 1;
auto authSize = readBlobInt(loginData);
this->username = username;
this->authData =
std::vector<uint8_t>(loginData.begin() + blobSkipPosition,
loginData.begin() + blobSkipPosition + authSize);
}
void LoginBlob::loadUserPass(const std::string &username, const std::string &password)
{
this->username = username;
this->authData = std::vector<uint8_t>(password.begin(), password.end());
this->authType = static_cast<uint32_t>(AuthenticationType_AUTHENTICATION_USER_PASS);
void LoginBlob::loadUserPass(const std::string& username,
const std::string& password) {
this->username = username;
this->authData = std::vector<uint8_t>(password.begin(), password.end());
this->authType =
static_cast<uint32_t>(AuthenticationType_AUTHENTICATION_USER_PASS);
}
void LoginBlob::loadJson(const std::string &json)
{
auto root = cJSON_Parse(json.c_str());
auto authTypeObject = cJSON_GetObjectItemCaseSensitive(root, "authType");
auto usernameObject = cJSON_GetObjectItemCaseSensitive(root, "username");
auto authDataObject = cJSON_GetObjectItemCaseSensitive(root, "authData");
void LoginBlob::loadJson(const std::string& json) {
auto root = nlohmann::json::parse(json);
this->authType = root["authType"];
this->username = root["username"];
std::string authDataObject = root["authData"];
auto authDataString = std::string(cJSON_GetStringValue(authDataObject));
this->authData = crypto->base64Decode(authDataString);
this->username = std::string(cJSON_GetStringValue(usernameObject));
this->authType = cJSON_GetNumberValue(authTypeObject);
cJSON_Delete(root);
this->authData = crypto->base64Decode(authDataObject);
}
std::string LoginBlob::toJson()
{
bell::JSONObject obj;
obj["authData"] = crypto->base64Encode(authData);
obj["authType"] = this->authType;
obj["username"] = this->username;
return obj.toString();
std::string LoginBlob::toJson() {
nlohmann::json obj;
obj["authData"] = crypto->base64Encode(authData);
obj["authType"] = this->authType;
obj["username"] = this->username;
return obj.dump();
}
void LoginBlob::loadZeroconfQuery(
std::map<std::string, std::string>& queryParams) {
// Get all urlencoded params
auto username = queryParams["userName"];
auto blobString = queryParams["blob"];
auto clientKeyString = queryParams["clientKey"];
auto deviceName = queryParams["deviceName"];
// client key and bytes are urlencoded
auto clientKeyBytes = crypto->base64Decode(clientKeyString);
auto blobBytes = crypto->base64Decode(blobString);
// Generated secret based on earlier generated DH
auto secretKey = crypto->dhCalculateShared(clientKeyBytes);
this->loadZeroconf(blobBytes, secretKey, deviceId, username);
}
std::string LoginBlob::buildZeroconfInfo() {
// Encode publicKey into base64
auto encodedKey = crypto->base64Encode(crypto->publicKey);
nlohmann::json obj;
obj["status"] = 101;
obj["statusString"] = "OK";
obj["version"] = cspot::protocolVersion;
obj["spotifyError"] = 0;
obj["libraryVersion"] = cspot::swVersion;
obj["accountReq"] = "PREMIUM";
obj["brandDisplayName"] = cspot::brandName;
obj["modelDisplayName"] = name;
obj["voiceSupport"] = "NO";
obj["availability"] = this->username;
obj["productID"] = 0;
obj["tokenType"] = "default";
obj["groupStatus"] = "NONE";
obj["resolverVersion"] = "0";
obj["scope"] = "streaming,client-authorization-universal";
obj["activeUser"] = "";
obj["deviceID"] = deviceId;
obj["remoteName"] = name;
obj["publicKey"] = encodedKey;
obj["deviceType"] = "SPEAKER";
return obj.dump();
}
std::string LoginBlob::getDeviceId() {
return this->deviceId;
}
std::string LoginBlob::getDeviceName() {
return this->name;
}
std::string LoginBlob::getUserName() {
return this->username;
}

View File

@@ -1,373 +0,0 @@
#include "MercuryManager.h"
#include <iostream>
#include "Logger.h"
std::map<MercuryType, std::string> MercuryTypeMap({
{MercuryType::GET, "GET"},
{MercuryType::SEND, "SEND"},
{MercuryType::SUB, "SUB"},
{MercuryType::UNSUB, "UNSUB"},
});
MercuryManager::MercuryManager(std::unique_ptr<Session> session): bell::Task("mercuryManager", 6 * 1024, 1, 1)
{
tempMercuryHeader = {};
this->timeProvider = std::make_shared<TimeProvider>();
this->callbacks = std::map<uint64_t, mercuryCallback>();
this->subscriptions = std::map<std::string, mercuryCallback>();
this->session = std::move(session);
this->sequenceId = 0x00000001;
this->audioChunkManager = std::make_unique<AudioChunkManager>();
this->audioChunkSequence = 0;
this->audioKeySequence = 0;
this->queue = std::vector<std::unique_ptr<Packet>>();
queueSemaphore = std::make_unique<WrappedSemaphore>(200);
this->session->shanConn->conn->timeoutHandler = [this]() {
return this->timeoutHandler();
};
}
MercuryManager::~MercuryManager()
{
//pb_release(Header_fields, &tempMercuryHeader);
}
bool MercuryManager::timeoutHandler()
{
if (!isRunning) return true;
auto currentTimestamp = timeProvider->getSyncedTimestamp();
if (this->lastRequestTimestamp != -1 && currentTimestamp - this->lastRequestTimestamp > AUDIOCHUNK_TIMEOUT_MS)
{
CSPOT_LOG(debug, "Reconnection required, no mercury response");
return true;
}
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS)
{
CSPOT_LOG(debug, "Reconnection required, no ping received");
return true;
}
return false;
}
void MercuryManager::unregisterMercuryCallback(uint64_t seqId)
{
auto element = this->callbacks.find(seqId);
if (element != this->callbacks.end())
{
this->callbacks.erase(element);
}
}
void MercuryManager::requestAudioKey(std::vector<uint8_t> trackId, std::vector<uint8_t> fileId, audioKeyCallback& audioCallback)
{
std::lock_guard<std::mutex> guard(reconnectionMutex);
auto buffer = fileId;
this->keyCallback = audioCallback;
// Structure: [FILEID] [TRACKID] [4 BYTES SEQUENCE ID] [0x00, 0x00]
buffer.insert(buffer.end(), trackId.begin(), trackId.end());
auto audioKeySequence = pack<uint32_t>(htonl(this->audioKeySequence));
buffer.insert(buffer.end(), audioKeySequence.begin(), audioKeySequence.end());
auto suffix = std::vector<uint8_t>({ 0x00, 0x00 });
buffer.insert(buffer.end(), suffix.begin(), suffix.end());
// Bump audio key sequence
this->audioKeySequence += 1;
// Used for broken connection detection
this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
this->session->shanConn->sendPacket(static_cast<uint8_t>(MercuryType::AUDIO_KEY_REQUEST_COMMAND), buffer);
}
void MercuryManager::freeAudioKeyCallback()
{
this->keyCallback = nullptr;
}
std::shared_ptr<AudioChunk> MercuryManager::fetchAudioChunk(std::vector<uint8_t> fileId, std::vector<uint8_t>& audioKey, uint16_t index)
{
return this->fetchAudioChunk(fileId, audioKey, index * AUDIO_CHUNK_SIZE / 4, (index + 1) * AUDIO_CHUNK_SIZE / 4);
}
std::shared_ptr<AudioChunk> MercuryManager::fetchAudioChunk(std::vector<uint8_t> fileId, std::vector<uint8_t>& audioKey, uint32_t startPos, uint32_t endPos)
{
std::lock_guard<std::mutex> guard(reconnectionMutex);
auto sampleStartBytes = pack<uint32_t>(htonl(startPos));
auto sampleEndBytes = pack<uint32_t>(htonl(endPos));
auto buffer = pack<uint16_t>(htons(this->audioChunkSequence));
auto hardcodedData = std::vector<uint8_t>(
{ 0x00, 0x01, // Channel num, currently just hardcoded to 1
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // bytes magic
0x00, 0x00, 0x9C, 0x40,
0x00, 0x02, 0x00, 0x00 });
buffer.insert(buffer.end(), hardcodedData.begin(), hardcodedData.end());
buffer.insert(buffer.end(), fileId.begin(), fileId.end());
buffer.insert(buffer.end(), sampleStartBytes.begin(), sampleStartBytes.end());
buffer.insert(buffer.end(), sampleEndBytes.begin(), sampleEndBytes.end());
// Bump chunk sequence
this->audioChunkSequence += 1;
this->session->shanConn->sendPacket(static_cast<uint8_t>(MercuryType::AUDIO_CHUNK_REQUEST_COMMAND), buffer);
// Used for broken connection detection
//CSPOT_LOG(info, "requesting Chunk %hu", this->audioChunkSequence - 1);
this->lastRequestTimestamp = this->timeProvider->getSyncedTimestamp();
return this->audioChunkManager->registerNewChunk(this->audioChunkSequence - 1, audioKey, startPos, endPos);
}
void MercuryManager::reconnect()
{
std::lock_guard<std::mutex> guard(this->reconnectionMutex);
this->lastPingTimestamp = -1;
this->lastRequestTimestamp = -1;
RECONNECT:
if (!isRunning) return;
CSPOT_LOG(debug, "Trying to reconnect...");
try
{
if (this->session->shanConn->conn != nullptr)
{
this->session->shanConn->conn->timeoutHandler = nullptr;
}
this->audioChunkManager->failAllChunks();
if (this->session->authBlob != nullptr)
{
this->lastAuthBlob = this->session->authBlob;
}
this->session = std::make_unique<Session>();
this->session->connectWithRandomAp();
this->session->authenticate(this->lastAuthBlob);
this->session->shanConn->conn->timeoutHandler = [this]() {
return this->timeoutHandler();
};
CSPOT_LOG(debug, "Reconnected successfuly :)");
}
catch (...)
{
CSPOT_LOG(debug, "Reconnection failed, willl retry in %d secs", RECONNECTION_RETRY_MS / 1000);
usleep(RECONNECTION_RETRY_MS * 1000);
goto RECONNECT;
//reconnect();
}
}
void MercuryManager::runTask()
{
std::scoped_lock lock(this->runningMutex);
// Listen for mercury replies and handle them accordingly
isRunning = true;
while (isRunning)
{
std::unique_ptr<Packet> packet;
try
{
packet = this->session->shanConn->recvPacket();
}
catch (const std::runtime_error& e)
{
if (!isRunning) break;
// Reconnection required
this->reconnect();
this->reconnectedCallback();
continue;
}
if (static_cast<MercuryType>(packet->command) == MercuryType::PING) // @TODO: Handle time synchronization through ping
{
this->timeProvider->syncWithPingPacket(packet->data);
this->lastPingTimestamp = this->timeProvider->getSyncedTimestamp();
this->session->shanConn->sendPacket(0x49, packet->data);
}
else if (static_cast<MercuryType>(packet->command) == MercuryType::AUDIO_CHUNK_SUCCESS_RESPONSE)
{
this->lastRequestTimestamp = -1;
this->audioChunkManager->handleChunkData(packet->data, false);
}
else
{
this->queue.push_back(std::move(packet));
this->queueSemaphore->give();
}
}
}
void MercuryManager::stop() {
std::scoped_lock stop(this->stopMutex);
CSPOT_LOG(debug, "Stopping mercury manager");
isRunning = false;
audioChunkManager->close();
std::scoped_lock lock(this->runningMutex);
CSPOT_LOG(debug, "mercury stopped");
}
void MercuryManager::updateQueue() {
if (queueSemaphore->twait() == 0) {
if (this->queue.size() > 0)
{
std::unique_ptr<Packet> packet = std::move(this->queue[0]);
this->queue.erase(this->queue.begin());
if(packet == nullptr){
return;
}
CSPOT_LOG(debug, "Received packet with code %d of length %d", packet->command, packet->data.size());
switch (static_cast<MercuryType>(packet->command))
{
case MercuryType::COUNTRY_CODE_RESPONSE:
{
memcpy(countryCode, packet->data.data(), 2);
CSPOT_LOG(debug, "Received country code: %.2s", countryCode);
break;
}
case MercuryType::AUDIO_KEY_FAILURE_RESPONSE:
case MercuryType::AUDIO_KEY_SUCCESS_RESPONSE:
{
this->lastRequestTimestamp = -1;
// First four bytes mark the sequence id
auto seqId = ntohl(extract<uint32_t>(packet->data, 0));
if (seqId == (this->audioKeySequence - 1) && this->keyCallback != nullptr)
{
auto success = static_cast<MercuryType>(packet->command) == MercuryType::AUDIO_KEY_SUCCESS_RESPONSE;
this->keyCallback(success, packet->data);
}
break;
}
case MercuryType::AUDIO_CHUNK_FAILURE_RESPONSE:
{
CSPOT_LOG(error, "Audio Chunk failure!");
this->audioChunkManager->handleChunkData(packet->data, true);
this->lastRequestTimestamp = -1;
break;
}
case MercuryType::SEND:
case MercuryType::SUB:
case MercuryType::UNSUB:
{
auto response = std::make_unique<MercuryResponse>(packet->data);
if (response->parts.size() > 0)
{
CSPOT_LOG(debug, " MercuryType::UNSUB response->parts[0].size() = %d", response->parts[0].size());
}
if (this->callbacks.count(response->sequenceId) > 0)
{
auto seqId = response->sequenceId;
this->callbacks[response->sequenceId](std::move(response));
this->callbacks.erase(this->callbacks.find(seqId));
}
break;
}
case MercuryType::SUBRES:
{
auto response = std::make_unique<MercuryResponse>(packet->data);
auto uri = std::string(response->mercuryHeader.uri);
if (this->subscriptions.count(uri) > 0)
{
this->subscriptions[uri](std::move(response));
//this->subscriptions.erase(std::string(response->mercuryHeader.uri));
}
break;
}
default:
break;
}
}
}
}
void MercuryManager::handleQueue()
{
while (isRunning)
{
this->updateQueue();
}
std::scoped_lock lock(this->stopMutex);
}
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryCallback& subscription, mercuryParts& payload)
{
if (!isRunning) return -1;
std::lock_guard<std::mutex> guard(reconnectionMutex);
// Construct mercury header
CSPOT_LOG(debug, "executing MercuryType %s", MercuryTypeMap[method].c_str());
pbPutString(uri, tempMercuryHeader.uri);
pbPutString(MercuryTypeMap[method], tempMercuryHeader.method);
tempMercuryHeader.has_method = true;
tempMercuryHeader.has_uri = true;
// GET and SEND are actually the same. Therefore the override
// The difference between them is only in header's method
if (method == MercuryType::GET)
{
method = MercuryType::SEND;
}
auto headerBytes = pbEncode(Header_fields, &tempMercuryHeader);
// Register a subscription when given method is called
if (method == MercuryType::SUB)
{
this->subscriptions.insert({ uri, subscription });
}
this->callbacks.insert({ sequenceId, callback });
// Structure: [Sequence size] [SequenceId] [0x1] [Payloads number]
// [Header size] [Header] [Payloads (size + data)]
// Pack sequenceId
auto sequenceIdBytes = pack<uint64_t>(hton64(this->sequenceId));
auto sequenceSizeBytes = pack<uint16_t>(htons(sequenceIdBytes.size()));
sequenceIdBytes.insert(sequenceIdBytes.begin(), sequenceSizeBytes.begin(), sequenceSizeBytes.end());
sequenceIdBytes.push_back(0x01);
auto payloadNum = pack<uint16_t>(htons(payload.size() + 1));
sequenceIdBytes.insert(sequenceIdBytes.end(), payloadNum.begin(), payloadNum.end());
auto headerSizePayload = pack<uint16_t>(htons(headerBytes.size()));
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(), headerSizePayload.end());
sequenceIdBytes.insert(sequenceIdBytes.end(), headerBytes.begin(), headerBytes.end());
// Encode all the payload parts
for (int x = 0; x < payload.size(); x++)
{
headerSizePayload = pack<uint16_t>(htons(payload[x].size()));
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(), headerSizePayload.end());
sequenceIdBytes.insert(sequenceIdBytes.end(), payload[x].begin(), payload[x].end());
}
// Bump sequence id
this->sequenceId += 1;
this->session->shanConn->sendPacket(static_cast<std::underlying_type<MercuryType>::type>(method), sequenceIdBytes);
return this->sequenceId - 1;
}
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryParts& payload)
{
mercuryCallback subscription = nullptr;
return this->execute(method, uri, callback, subscription, payload);
}
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback, mercuryCallback& subscription)
{
auto payload = mercuryParts(0);
return this->execute(method, uri, callback, subscription, payload);
}
uint64_t MercuryManager::execute(MercuryType method, std::string uri, mercuryCallback& callback)
{
auto payload = mercuryParts(0);
return this->execute(method, uri, callback, payload);
}

View File

@@ -1,38 +0,0 @@
#include "MercuryResponse.h"
MercuryResponse::MercuryResponse(std::vector<uint8_t> &data)
{
// this->mercuryHeader = std::make_unique<Header>();
this->mercuryHeader = {};
this->parts = mercuryParts(0);
this->parseResponse(data);
}
MercuryResponse::~MercuryResponse() {
}
void MercuryResponse::parseResponse(std::vector<uint8_t> &data)
{
auto sequenceLength = ntohs(extract<uint16_t>(data, 0));
this->sequenceId = hton64(extract<uint64_t>(data, 2));
auto partsNumber = ntohs(extract<uint16_t>(data, 11));
auto headerSize = ntohs(extract<uint16_t>(data, 13));
auto headerBytes = std::vector<uint8_t>(data.begin() + 15, data.begin() + 15 + headerSize);
auto pos = 15 + headerSize;
while (pos < data.size())
{
auto partSize = ntohs(extract<uint16_t>(data, pos));
this->parts.push_back(
std::vector<uint8_t>(
data.begin() + pos + 2,
data.begin() + pos + 2 + partSize));
pos += 2 + partSize;
}
pb_release(Header_fields, &this->mercuryHeader);
pbDecode(this->mercuryHeader, Header_fields, headerBytes);
}

View File

@@ -0,0 +1,306 @@
#include "MercurySession.h"
#include <memory>
#include <mutex>
#include "BellLogger.h"
#include "BellTask.h"
#include "BellUtils.h"
#include "CSpotContext.h"
#include "Logger.h"
using namespace cspot;
MercurySession::MercurySession(std::shared_ptr<TimeProvider> timeProvider)
: bell::Task("mercury_dispatcher", 4 * 1024, 3, 1) {
this->timeProvider = timeProvider;
}
MercurySession::~MercurySession() {
std::scoped_lock lock(this->isRunningMutex);
}
void MercurySession::runTask() {
isRunning = true;
std::scoped_lock lock(this->isRunningMutex);
this->executeEstabilishedCallback = true;
while (isRunning) {
cspot::Packet packet = {};
try {
packet = shanConn->recvPacket();
CSPOT_LOG(info, "Received packet, command: %d", packet.command);
if (static_cast<RequestType>(packet.command) == RequestType::PING) {
timeProvider->syncWithPingPacket(packet.data);
this->lastPingTimestamp = timeProvider->getSyncedTimestamp();
this->shanConn->sendPacket(0x49, packet.data);
} else {
this->packetQueue.push(packet);
}
} catch (const std::runtime_error& e) {
CSPOT_LOG(error, "Error while receiving packet: %s", e.what());
failAllPending();
if (!isRunning)
return;
reconnect();
continue;
}
}
}
void MercurySession::reconnect() {
isReconnecting = true;
try {
this->conn = nullptr;
this->shanConn = nullptr;
this->connectWithRandomAp();
this->authenticate(this->authBlob);
CSPOT_LOG(info, "Reconnection successful");
BELL_SLEEP_MS(100);
lastPingTimestamp = timeProvider->getSyncedTimestamp();
isReconnecting = false;
this->executeEstabilishedCallback = true;
} catch (...) {
CSPOT_LOG(error, "Cannot reconnect, will retry in 5s");
BELL_SLEEP_MS(5000);
if (isRunning) {
return reconnect();
}
}
}
void MercurySession::setConnectedHandler(
ConnectionEstabilishedCallback callback) {
this->connectionReadyCallback = callback;
}
bool MercurySession::triggerTimeout() {
if (!isRunning)
return true;
auto currentTimestamp = timeProvider->getSyncedTimestamp();
if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS) {
CSPOT_LOG(debug, "Reconnection required, no ping received");
return true;
}
return false;
}
void MercurySession::disconnect() {
CSPOT_LOG(info, "Disconnecting mercury session");
this->isRunning = false;
conn->close();
std::scoped_lock lock(this->isRunningMutex);
}
std::string MercurySession::getCountryCode() {
return this->countryCode;
}
void MercurySession::handlePacket() {
Packet packet = {};
this->packetQueue.wtpop(packet, 200);
if (executeEstabilishedCallback && this->connectionReadyCallback != nullptr) {
executeEstabilishedCallback = false;
this->connectionReadyCallback();
}
switch (static_cast<RequestType>(packet.command)) {
case RequestType::COUNTRY_CODE_RESPONSE: {
this->countryCode = std::string();
this->countryCode.reserve(2);
memcpy(this->countryCode.data(), packet.data.data(), 2);
CSPOT_LOG(debug, "Received country code");
break;
}
case RequestType::AUDIO_KEY_FAILURE_RESPONSE:
case RequestType::AUDIO_KEY_SUCCESS_RESPONSE: {
// this->lastRequestTimestamp = -1;
// First four bytes mark the sequence id
auto seqId = ntohl(extract<uint32_t>(packet.data, 0));
if (seqId == (this->audioKeySequence - 1) &&
audioKeyCallback != nullptr) {
auto success = static_cast<RequestType>(packet.command) ==
RequestType::AUDIO_KEY_SUCCESS_RESPONSE;
audioKeyCallback(success, packet.data);
}
break;
}
case RequestType::SEND:
case RequestType::SUB:
case RequestType::UNSUB: {
CSPOT_LOG(debug, "Received mercury packet");
auto response = this->decodeResponse(packet.data);
if (this->callbacks.count(response.sequenceId) > 0) {
auto seqId = response.sequenceId;
this->callbacks[response.sequenceId](response);
this->callbacks.erase(this->callbacks.find(seqId));
}
break;
}
case RequestType::SUBRES: {
auto response = decodeResponse(packet.data);
auto uri = std::string(response.mercuryHeader.uri);
if (this->subscriptions.count(uri) > 0) {
this->subscriptions[uri](response);
}
break;
}
default:
break;
}
}
void MercurySession::failAllPending() {
Response response = { };
response.fail = true;
// Fail all callbacks
for (auto& it : this->callbacks) {
it.second(response);
}
// Fail all subscriptions
for (auto& it : this->subscriptions) {
it.second(response);
}
// Remove references
this->subscriptions = {};
this->callbacks = {};
}
MercurySession::Response MercurySession::decodeResponse(
const std::vector<uint8_t>& data) {
Response response = {};
response.parts = {};
auto sequenceLength = ntohs(extract<uint16_t>(data, 0));
response.sequenceId = hton64(extract<uint64_t>(data, 2));
auto partsNumber = ntohs(extract<uint16_t>(data, 11));
auto headerSize = ntohs(extract<uint16_t>(data, 13));
auto headerBytes =
std::vector<uint8_t>(data.begin() + 15, data.begin() + 15 + headerSize);
auto pos = 15 + headerSize;
while (pos < data.size()) {
auto partSize = ntohs(extract<uint16_t>(data, pos));
response.parts.push_back(std::vector<uint8_t>(
data.begin() + pos + 2, data.begin() + pos + 2 + partSize));
pos += 2 + partSize;
}
pbDecode(response.mercuryHeader, Header_fields, headerBytes);
response.fail = false;
return response;
}
uint64_t MercurySession::executeSubscription(RequestType method,
const std::string& uri,
ResponseCallback callback,
ResponseCallback subscription,
DataParts& payload) {
CSPOT_LOG(debug, "Executing Mercury Request, type %s",
RequestTypeMap[method].c_str());
// Encode header
pbPutString(uri, tempMercuryHeader.uri);
pbPutString(RequestTypeMap[method], tempMercuryHeader.method);
tempMercuryHeader.has_method = true;
tempMercuryHeader.has_uri = true;
// GET and SEND are actually the same. Therefore the override
// The difference between them is only in header's method
if (method == RequestType::GET) {
method = RequestType::SEND;
}
if (method == RequestType::SUB) {
this->subscriptions.insert({uri, subscription});
}
auto headerBytes = pbEncode(Header_fields, &tempMercuryHeader);
this->callbacks.insert({sequenceId, callback});
// Structure: [Sequence size] [SequenceId] [0x1] [Payloads number]
// [Header size] [Header] [Payloads (size + data)]
// Pack sequenceId
auto sequenceIdBytes = pack<uint64_t>(hton64(this->sequenceId));
auto sequenceSizeBytes = pack<uint16_t>(htons(sequenceIdBytes.size()));
sequenceIdBytes.insert(sequenceIdBytes.begin(), sequenceSizeBytes.begin(),
sequenceSizeBytes.end());
sequenceIdBytes.push_back(0x01);
auto payloadNum = pack<uint16_t>(htons(payload.size() + 1));
sequenceIdBytes.insert(sequenceIdBytes.end(), payloadNum.begin(),
payloadNum.end());
auto headerSizePayload = pack<uint16_t>(htons(headerBytes.size()));
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
headerSizePayload.end());
sequenceIdBytes.insert(sequenceIdBytes.end(), headerBytes.begin(),
headerBytes.end());
// Encode all the payload parts
for (int x = 0; x < payload.size(); x++) {
headerSizePayload = pack<uint16_t>(htons(payload[x].size()));
sequenceIdBytes.insert(sequenceIdBytes.end(), headerSizePayload.begin(),
headerSizePayload.end());
sequenceIdBytes.insert(sequenceIdBytes.end(), payload[x].begin(),
payload[x].end());
}
// Bump sequence id
this->sequenceId += 1;
this->shanConn->sendPacket(
static_cast<std::underlying_type<RequestType>::type>(method),
sequenceIdBytes);
return this->sequenceId - 1;
}
void MercurySession::requestAudioKey(const std::vector<uint8_t>& trackId,
const std::vector<uint8_t>& fileId,
AudioKeyCallback audioCallback) {
auto buffer = fileId;
this->audioKeyCallback = audioCallback;
// Structure: [FILEID] [TRACKID] [4 BYTES SEQUENCE ID] [0x00, 0x00]
buffer.insert(buffer.end(), trackId.begin(), trackId.end());
auto audioKeySequence = pack<uint32_t>(htonl(this->audioKeySequence));
buffer.insert(buffer.end(), audioKeySequence.begin(), audioKeySequence.end());
auto suffix = std::vector<uint8_t>({0x00, 0x00});
buffer.insert(buffer.end(), suffix.begin(), suffix.end());
// Bump audio key sequence
this->audioKeySequence += 1;
// Used for broken connection detection
// this->lastRequestTimestamp = timeProvider->getSyncedTimestamp();
this->shanConn->sendPacket(
static_cast<uint8_t>(RequestType::AUDIO_KEY_REQUEST_COMMAND), buffer);
}

View File

@@ -1,6 +0,0 @@
#include "Packet.h"
Packet::Packet(uint8_t command, const std::vector<uint8_t> &data) {
this->command = command;
this->data = data;
};

View File

@@ -1,4 +1,3 @@
#include "PlainConnection.h"
#include <cstring>
#ifdef _WIN32
@@ -9,191 +8,188 @@
#include <errno.h>
#include "Logger.h"
static int getErrno()
{
using namespace cspot;
static int getErrno() {
#ifdef _WIN32
int code = WSAGetLastError();
if (code == WSAETIMEDOUT) return ETIMEDOUT;
if (code == WSAEINTR) return EINTR;
return code;
int code = WSAGetLastError();
if (code == WSAETIMEDOUT)
return ETIMEDOUT;
if (code == WSAEINTR)
return EINTR;
return code;
#else
return errno;
return errno;
#endif
}
PlainConnection::PlainConnection()
{
this->apSock = -1;
PlainConnection::PlainConnection() {
this->apSock = -1;
};
PlainConnection::~PlainConnection()
{
closeSocket();
PlainConnection::~PlainConnection() {
this->close();
};
void PlainConnection::connectToAp(std::string apAddress)
{
struct addrinfo h, *airoot, *ai;
std::string hostname = apAddress.substr(0, apAddress.find(":"));
std::string portStr = apAddress.substr(apAddress.find(":") + 1, apAddress.size());
memset(&h, 0, sizeof(h));
h.ai_family = AF_INET;
h.ai_socktype = SOCK_STREAM;
h.ai_protocol = IPPROTO_IP;
void PlainConnection::connect(const std::string& apAddress) {
struct addrinfo h, *airoot, *ai;
std::string hostname = apAddress.substr(0, apAddress.find(":"));
std::string portStr =
apAddress.substr(apAddress.find(":") + 1, apAddress.size());
memset(&h, 0, sizeof(h));
h.ai_family = AF_INET;
h.ai_socktype = SOCK_STREAM;
h.ai_protocol = IPPROTO_IP;
// Lookup host
if (getaddrinfo(hostname.c_str(), portStr.c_str(), &h, &airoot))
{
CSPOT_LOG(error, "getaddrinfo failed");
}
// Lookup host
if (getaddrinfo(hostname.c_str(), portStr.c_str(), &h, &airoot)) {
CSPOT_LOG(error, "getaddrinfo failed");
}
// find the right ai, connect to server
for (ai = airoot; ai; ai = ai->ai_next)
{
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
continue;
// find the right ai, connect to server
for (ai = airoot; ai; ai = ai->ai_next) {
if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
continue;
this->apSock = socket(ai->ai_family,
ai->ai_socktype, ai->ai_protocol);
if (this->apSock < 0)
continue;
this->apSock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (this->apSock < 0)
continue;
if (connect(this->apSock,
(struct sockaddr *)ai->ai_addr,
ai->ai_addrlen) != -1)
{
if (::connect(this->apSock, (struct sockaddr*)ai->ai_addr, ai->ai_addrlen) !=
-1) {
#ifdef _WIN32
uint32_t tv = 3000;
uint32_t tv = 3000;
#else
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
#endif
setsockopt(this->apSock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
setsockopt(this->apSock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof tv);
setsockopt(this->apSock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv,
sizeof tv);
setsockopt(this->apSock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv,
sizeof tv);
int flag = 1;
setsockopt(this->apSock, /* socket affected */
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(char *)&flag, /* the cast is historical cruft */
sizeof(int)); /* length of option value */
break;
}
close(this->apSock);
apSock = -1;
throw std::runtime_error("Can't connect to spotify servers");
int flag = 1;
setsockopt(this->apSock, /* socket affected */
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(char*)&flag, /* the cast is historical cruft */
sizeof(int)); /* length of option value */
break;
}
freeaddrinfo(airoot);
CSPOT_LOG(debug, "Connected to spotify server");
#ifdef _WIN32
closesocket(this->apSock);
#else
::close(this->apSock);
#endif
apSock = -1;
throw std::runtime_error("Can't connect to spotify servers");
}
freeaddrinfo(airoot);
CSPOT_LOG(debug, "Connected to spotify server");
}
std::vector<uint8_t> PlainConnection::recvPacket()
{
// Read packet size
auto sizeData = readBlock(4);
uint32_t packetSize = ntohl(extract<uint32_t>(sizeData, 0));
// Read actual data
auto data = readBlock(packetSize - 4);
sizeData.insert(sizeData.end(), data.begin(), data.end());
std::vector<uint8_t> PlainConnection::recvPacket() {
// Read packet size
std::vector<uint8_t> packetBuffer(4);
readBlock(packetBuffer.data(), 4);
uint32_t packetSize = ntohl(extract<uint32_t>(packetBuffer, 0));
return sizeData;
packetBuffer.resize(packetSize, 0);
// Read actual data
readBlock(packetBuffer.data() + 4, packetSize - 4);
return packetBuffer;
}
std::vector<uint8_t> PlainConnection::sendPrefixPacket(const std::vector<uint8_t> &prefix, const std::vector<uint8_t> &data)
{
// Calculate full packet length
uint32_t actualSize = prefix.size() + data.size() + sizeof(uint32_t);
std::vector<uint8_t> PlainConnection::sendPrefixPacket(
const std::vector<uint8_t>& prefix, const std::vector<uint8_t>& data) {
// Calculate full packet length
uint32_t actualSize = prefix.size() + data.size() + sizeof(uint32_t);
// Packet structure [PREFIX] + [SIZE] + [DATA]
auto sizeRaw = pack<uint32_t>(htonl(actualSize));
sizeRaw.insert(sizeRaw.begin(), prefix.begin(), prefix.end());
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
// Packet structure [PREFIX] + [SIZE] + [DATA]
auto sizeRaw = pack<uint32_t>(htonl(actualSize));
sizeRaw.insert(sizeRaw.begin(), prefix.begin(), prefix.end());
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
// Actually write it to the server
writeBlock(sizeRaw);
// Actually write it to the server
writeBlock(sizeRaw);
return sizeRaw;
return sizeRaw;
}
std::vector<uint8_t> PlainConnection::readBlock(size_t size)
{
std::vector<uint8_t> buf(size);
unsigned int idx = 0;
ssize_t n;
int retries = 0;
// printf("START READ\n");
void PlainConnection::readBlock(const uint8_t* dst, size_t size) {
unsigned int idx = 0;
ssize_t n;
int retries = 0;
while (idx < size)
{
READ:
if ((n = recv(this->apSock, (char*) &buf[idx], size - idx, 0)) <= 0)
{
switch (getErrno())
{
case EAGAIN:
case ETIMEDOUT:
if (timeoutHandler())
{
CSPOT_LOG(error, "Connection lost, will need to reconnect...");
throw std::runtime_error("Reconnection required");
}
goto READ;
case EINTR:
break;
default:
if (retries++ > 4) throw std::runtime_error("Error in read");
goto READ;
}
}
idx += n;
while (idx < size) {
READ:
if ((n = recv(this->apSock, (char*)&dst[idx], size - idx, 0)) <= 0) {
switch (getErrno()) {
case EAGAIN:
case ETIMEDOUT:
// if (timeoutHandler()) {
// CSPOT_LOG(error, "Connection lost, will need to reconnect...");
// throw std::runtime_error("Reconnection required");
// }
goto READ;
case EINTR:
break;
default:
if (retries++ > 4)
throw std::runtime_error("Error in read");
goto READ;
}
}
// printf("FINISH READ\n");
return buf;
idx += n;
}
}
size_t PlainConnection::writeBlock(const std::vector<uint8_t> &data)
{
unsigned int idx = 0;
ssize_t n;
// printf("START WRITE\n");
int retries = 0;
size_t PlainConnection::writeBlock(const std::vector<uint8_t>& data) {
unsigned int idx = 0;
ssize_t n;
while (idx < data.size())
{
WRITE:
if ((n = send(this->apSock, (char*) &data[idx], data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0)
{
switch (getErrno())
{
case EAGAIN:
case ETIMEDOUT:
if (timeoutHandler())
{
throw std::runtime_error("Reconnection required");
}
goto WRITE;
case EINTR:
break;
default:
if (retries++ > 4) throw std::runtime_error("Error in write");
goto WRITE;
}
}
idx += n;
int retries = 0;
while (idx < data.size()) {
WRITE:
if ((n = send(this->apSock, (char*)&data[idx],
data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0) {
switch (getErrno()) {
case EAGAIN:
case ETIMEDOUT:
// if (timeoutHandler()) {
// throw std::runtime_error("Reconnection required");
// }
goto WRITE;
case EINTR:
break;
default:
if (retries++ > 4)
throw std::runtime_error("Error in write");
goto WRITE;
}
}
idx += n;
}
return data.size();
return data.size();
}
void PlainConnection::closeSocket()
{
if (this->apSock < 0) return;
void PlainConnection::close() {
if (this->apSock < 0)
return;
CSPOT_LOG(info, "Closing socket...");
shutdown(this->apSock, SHUT_RDWR);
close(this->apSock);
this->apSock = -1;
CSPOT_LOG(info, "Closing socket...");
shutdown(this->apSock, SHUT_RDWR);
#ifdef _WIN32
closesocket(this->apSock);
#else
::close(this->apSock);
#endif
this->apSock = -1;
}

View File

@@ -0,0 +1,299 @@
#include "PlaybackState.h"
#include <memory>
#include "CSpotContext.h"
#include "Logger.h"
using namespace cspot;
PlaybackState::PlaybackState(std::shared_ptr<cspot::Context> ctx) {
this->ctx = ctx;
innerFrame = {};
remoteFrame = {};
// Prepare default state
innerFrame.state.has_position_ms = true;
innerFrame.state.position_ms = 0;
innerFrame.state.status = PlayStatus_kPlayStatusStop;
innerFrame.state.has_status = true;
innerFrame.state.position_measured_at = 0;
innerFrame.state.has_position_measured_at = true;
innerFrame.state.shuffle = false;
innerFrame.state.has_shuffle = true;
innerFrame.state.repeat = false;
innerFrame.state.has_repeat = true;
innerFrame.device_state.sw_version = strdup(swVersion);
innerFrame.device_state.is_active = false;
innerFrame.device_state.has_is_active = true;
innerFrame.device_state.can_play = true;
innerFrame.device_state.has_can_play = true;
innerFrame.device_state.volume = ctx->config.volume;
innerFrame.device_state.has_volume = true;
innerFrame.device_state.name = strdup(ctx->config.deviceName.c_str());
innerFrame.state.track_count = 0;
// Prepare player's capabilities
addCapability(CapabilityType_kCanBePlayer, 1);
addCapability(CapabilityType_kDeviceType, 4);
addCapability(CapabilityType_kGaiaEqConnectId, 1);
addCapability(CapabilityType_kSupportsLogout, 0);
addCapability(CapabilityType_kIsObservable, 1);
addCapability(CapabilityType_kVolumeSteps, 64);
addCapability(CapabilityType_kSupportedContexts, -1,
std::vector<std::string>({"album", "playlist", "search",
"inbox", "toplist", "starred",
"publishedstarred", "track"}));
addCapability(CapabilityType_kSupportedTypes, -1,
std::vector<std::string>({"audio/local", "audio/track",
"audio/episode", "local", "track"}));
innerFrame.device_state.capabilities_count = 8;
}
PlaybackState::~PlaybackState() {
pb_release(Frame_fields, &innerFrame);
pb_release(Frame_fields, &remoteFrame);
}
void PlaybackState::setPlaybackState(const PlaybackState::State state) {
switch (state) {
case State::Loading:
// Prepare the playback at position 0
innerFrame.state.status = PlayStatus_kPlayStatusPause;
innerFrame.state.position_ms = 0;
innerFrame.state.position_measured_at =
ctx->timeProvider->getSyncedTimestamp();
break;
case State::Playing:
innerFrame.state.status = PlayStatus_kPlayStatusPlay;
innerFrame.state.position_measured_at =
ctx->timeProvider->getSyncedTimestamp();
break;
case State::Stopped:
break;
case State::Paused:
// Update state and recalculate current song position
innerFrame.state.status = PlayStatus_kPlayStatusPause;
uint32_t diff = ctx->timeProvider->getSyncedTimestamp() -
innerFrame.state.position_measured_at;
this->updatePositionMs(innerFrame.state.position_ms + diff);
break;
}
}
bool PlaybackState::isActive() {
return innerFrame.device_state.is_active;
}
bool PlaybackState::nextTrack() {
innerFrame.state.playing_track_index++;
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count) {
innerFrame.state.playing_track_index = 0;
if (!innerFrame.state.repeat) {
setPlaybackState(State::Paused);
return false;
}
}
return true;
}
void PlaybackState::prevTrack() {
if (innerFrame.state.playing_track_index > 0) {
innerFrame.state.playing_track_index--;
} else if (innerFrame.state.repeat) {
innerFrame.state.playing_track_index = innerFrame.state.track_count - 1;
}
}
void PlaybackState::setActive(bool isActive) {
innerFrame.device_state.is_active = isActive;
if (isActive) {
innerFrame.device_state.became_active_at =
ctx->timeProvider->getSyncedTimestamp();
innerFrame.device_state.has_became_active_at = true;
}
}
void PlaybackState::updatePositionMs(uint32_t position) {
innerFrame.state.position_ms = position;
innerFrame.state.position_measured_at =
ctx->timeProvider->getSyncedTimestamp();
}
#define FREE(ptr) \
{ \
free(ptr); \
ptr = NULL; \
}
#define STRDUP(dst, src) \
if (src != NULL) { \
dst = strdup(src); \
} else { \
FREE(dst); \
} // strdup null pointer safe
void PlaybackState::updateTracks() {
CSPOT_LOG(info, "---- Track count %d", remoteFrame.state.track_count);
CSPOT_LOG(info, "---- Inner track count %d", innerFrame.state.track_count);
CSPOT_LOG(info, "--- Context URI %s", remoteFrame.state.context_uri);
// free unused tracks
if (innerFrame.state.track_count > remoteFrame.state.track_count) {
for (uint16_t i = remoteFrame.state.track_count;
i < innerFrame.state.track_count; ++i) {
FREE(innerFrame.state.track[i].gid);
FREE(innerFrame.state.track[i].uri);
FREE(innerFrame.state.track[i].context);
}
}
// reallocate memory for new tracks
innerFrame.state.track = (TrackRef*)realloc(
innerFrame.state.track, sizeof(TrackRef) * remoteFrame.state.track_count);
for (uint16_t i = 0; i < remoteFrame.state.track_count; ++i) {
if (i >= innerFrame.state.track_count) {
innerFrame.state.track[i].gid = NULL;
innerFrame.state.track[i].uri = NULL;
innerFrame.state.track[i].context = NULL;
}
if (remoteFrame.state.track[i].gid != NULL) {
uint16_t gid_size = remoteFrame.state.track[i].gid->size;
innerFrame.state.track[i].gid = (pb_bytes_array_t*)realloc(
innerFrame.state.track[i].gid, PB_BYTES_ARRAY_T_ALLOCSIZE(gid_size));
memcpy(innerFrame.state.track[i].gid->bytes,
remoteFrame.state.track[i].gid->bytes, gid_size);
innerFrame.state.track[i].gid->size = gid_size;
}
innerFrame.state.track[i].has_queued =
remoteFrame.state.track[i].has_queued;
innerFrame.state.track[i].queued = remoteFrame.state.track[i].queued;
STRDUP(innerFrame.state.track[i].uri, remoteFrame.state.track[i].uri);
STRDUP(innerFrame.state.track[i].context,
remoteFrame.state.track[i].context);
}
innerFrame.state.context_uri = (char*)realloc(
innerFrame.state.context_uri, strlen(remoteFrame.state.context_uri) + 1);
strcpy(innerFrame.state.context_uri, remoteFrame.state.context_uri);
innerFrame.state.track_count = remoteFrame.state.track_count;
innerFrame.state.has_playing_track_index = true;
innerFrame.state.playing_track_index = remoteFrame.state.playing_track_index;
if (remoteFrame.state.repeat) {
setRepeat(true);
}
if (remoteFrame.state.shuffle) {
setShuffle(true);
}
}
void PlaybackState::setVolume(uint32_t volume) {
innerFrame.device_state.volume = volume;
ctx->config.volume = volume;
}
void PlaybackState::setShuffle(bool shuffle) {
innerFrame.state.shuffle = shuffle;
if (shuffle) {
// Put current song at the begining
std::swap(innerFrame.state.track[0],
innerFrame.state.track[innerFrame.state.playing_track_index]);
// Shuffle current tracks
for (int x = 1; x < innerFrame.state.track_count - 1; x++) {
auto j = x + (std::rand() % (innerFrame.state.track_count - x));
std::swap(innerFrame.state.track[j], innerFrame.state.track[x]);
}
innerFrame.state.playing_track_index = 0;
}
}
void PlaybackState::setRepeat(bool repeat) {
innerFrame.state.repeat = repeat;
}
TrackRef* PlaybackState::getCurrentTrackRef() {
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count) {
return nullptr;
}
return &innerFrame.state.track[innerFrame.state.playing_track_index];
}
TrackRef* PlaybackState::getNextTrackRef() {
if ((innerFrame.state.playing_track_index + 1) >= innerFrame.state.track_count) {
if (innerFrame.state.repeat) {
return &innerFrame.state.track[0];
}
return nullptr;
}
return &innerFrame.state.track[innerFrame.state.playing_track_index + 1];
}
std::vector<uint8_t> PlaybackState::encodeCurrentFrame(MessageType typ) {
free(innerFrame.ident);
free(innerFrame.protocol_version);
// Prepare current frame info
innerFrame.version = 1;
innerFrame.ident = strdup(ctx->config.deviceId.c_str());
innerFrame.seq_nr = this->seqNum;
innerFrame.protocol_version = strdup(protocolVersion);
innerFrame.typ = typ;
innerFrame.state_update_id = ctx->timeProvider->getSyncedTimestamp();
innerFrame.has_version = true;
innerFrame.has_seq_nr = true;
innerFrame.recipient_count = 0;
innerFrame.has_state = true;
innerFrame.has_device_state = true;
innerFrame.has_typ = true;
innerFrame.has_state_update_id = true;
this->seqNum += 1;
return pbEncode(Frame_fields, &innerFrame);
}
// Wraps messy nanopb setters. @TODO: find a better way to handle this
void PlaybackState::addCapability(CapabilityType typ, int intValue,
std::vector<std::string> stringValue) {
innerFrame.device_state.capabilities[capabilityIndex].has_typ = true;
this->innerFrame.device_state.capabilities[capabilityIndex].typ = typ;
if (intValue != -1) {
this->innerFrame.device_state.capabilities[capabilityIndex].intValue[0] =
intValue;
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
1;
} else {
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count =
0;
}
for (int x = 0; x < stringValue.size(); x++) {
pbPutString(stringValue[x],
this->innerFrame.device_state.capabilities[capabilityIndex]
.stringValue[x]);
}
this->innerFrame.device_state.capabilities[capabilityIndex]
.stringValue_count = stringValue.size();
this->capabilityIndex += 1;
}

View File

@@ -1,182 +0,0 @@
#include "Player.h"
#include "Logger.h"
// #include <valgrind/memcheck.h>
Player::Player(std::shared_ptr<MercuryManager> manager, std::shared_ptr<AudioSink> audioSink): bell::Task("player", 10 * 1024, -2, 1)
{
this->audioSink = audioSink;
this->manager = manager;
startTask();
}
void Player::pause()
{
if (currentTrack != nullptr)
{
if (currentTrack->audioStream != nullptr)
{
this->currentTrack->audioStream->isPaused = true;
}
}
}
void Player::play()
{
if (currentTrack != nullptr)
{
if (currentTrack->audioStream != nullptr)
{
this->currentTrack->audioStream->isPaused = false;
}
}
}
void Player::setVolume(uint32_t volume)
{
this->volume = (volume / (double)MAX_VOLUME) * 255;
// Calculate and cache log volume value
auto vol = 255 - this->volume;
uint32_t value = (log10(255 / ((float)vol + 1)) * 105.54571334);
if (value >= 254) value = 256;
logVolume = value << 8; // *256
// Pass volume event to the sink if volume is sink-handled
if (!this->audioSink->softwareVolumeControl)
{
this->audioSink->volumeChanged(volume);
}
}
void Player::seekMs(size_t positionMs)
{
if (currentTrack != nullptr)
{
if (currentTrack->audioStream != nullptr)
{
this->currentTrack->audioStream->seekMs(positionMs);
}
}
// VALGRIND_DO_LEAK_CHECK;
}
void Player::feedPCM(uint8_t *data, size_t len)
{
// Simple digital volume control alg
// @TODO actually extract it somewhere
if (this->audioSink->softwareVolumeControl)
{
int16_t* psample;
int32_t temp;
psample = (int16_t*)(data);
size_t half_len = len / 2;
for (uint32_t i = 0; i < half_len; i++)
{
// Offset data for unsigned sinks
if (this->audioSink->usign)
{
temp = ((int32_t)psample[i] + 0x8000) * logVolume;
}
else
{
temp = ((int32_t)psample[i]) * logVolume;
}
psample[i] = (temp >> 16) & 0xFFFF;
}
}
this->audioSink->feedPCMFrames(data, len);
}
void Player::runTask()
{
uint8_t *pcmOut = (uint8_t *) malloc(4096 / 4);
std::scoped_lock lock(this->runningMutex);
this->isRunning = true;
while (isRunning)
{
if(nextTrack != nullptr && nextTrack->loaded)
{
this->nextTrackMutex.lock();
currentTrack = this->nextTrack;
this->nextTrack = nullptr;
this->nextTrackMutex.unlock();
currentTrack->audioStream->startPlaybackLoop(pcmOut, 4096 / 4);
currentTrack->loadedTrackCallback = nullptr;
currentTrack->audioStream->streamFinishedCallback = nullptr;
currentTrack->audioStream->audioSink = nullptr;
currentTrack->audioStream->pcmCallback = nullptr;
delete currentTrack;
currentTrack = nullptr;
}
else
{
usleep(20000);
}
}
free(pcmOut);
}
void Player::stop() {
CSPOT_LOG(info, "Trying to stop");
this->isRunning = false;
cancelCurrentTrack();
std::scoped_lock lock(this->runningMutex);
if(this->nextTrack != nullptr)
{
delete this->nextTrack;
}
this->isRunning = false;
CSPOT_LOG(info, "Track cancelled");
cancelCurrentTrack();
CSPOT_LOG(info, "Stopping player");
}
void Player::cancelCurrentTrack()
{
if (currentTrack != nullptr)
{
if (currentTrack->audioStream != nullptr && currentTrack->audioStream->isRunning)
{
currentTrack->audioStream->isRunning = false;
}
}
}
void Player::handleLoad(std::shared_ptr<TrackReference> trackReference, std::function<void(bool)>& trackLoadedCallback, uint32_t position_ms, bool isPaused)
{
std::lock_guard<std::mutex> guard(loadTrackMutex);
pcmDataCallback framesCallback = [=](uint8_t *frames, size_t len) {
this->feedPCM(frames, len);
};
this->nextTrackMutex.lock();
if(this->nextTrack != nullptr)
{
delete this->nextTrack;
this->nextTrack = nullptr;
}
this->nextTrack = new SpotifyTrack(this->manager, trackReference, position_ms, isPaused);
this->nextTrack->trackInfoReceived = this->trackChanged;
this->nextTrack->loadedTrackCallback = [this, framesCallback, trackLoadedCallback]() {
bool needFlush = currentTrack != nullptr && currentTrack->audioStream != nullptr && currentTrack->audioStream->isRunning;
cancelCurrentTrack();
trackLoadedCallback(needFlush);
this->nextTrackMutex.lock();
this->nextTrack->audioStream->streamFinishedCallback = this->endOfFileCallback;
this->nextTrack->audioStream->audioSink = this->audioSink;
this->nextTrack->audioStream->pcmCallback = framesCallback;
this->nextTrack->loaded = true;
this->nextTrackMutex.unlock();
};
this->nextTrackMutex.unlock();
}

View File

@@ -1,286 +0,0 @@
#include "PlayerState.h"
#include "Logger.h"
#include "ConfigJSON.h"
PlayerState::PlayerState(std::shared_ptr<TimeProvider> timeProvider)
{
this->timeProvider = timeProvider;
innerFrame = {};
remoteFrame = {};
// Prepare default state
innerFrame.state.has_position_ms = true;
innerFrame.state.position_ms = 0;
innerFrame.state.status = PlayStatus_kPlayStatusStop;
innerFrame.state.has_status = true;
innerFrame.state.position_measured_at = 0;
innerFrame.state.has_position_measured_at = true;
innerFrame.state.shuffle = false;
innerFrame.state.has_shuffle = true;
innerFrame.state.repeat = false;
innerFrame.state.has_repeat = true;
innerFrame.device_state.sw_version = strdup(swVersion);
innerFrame.device_state.is_active = false;
innerFrame.device_state.has_is_active = true;
innerFrame.device_state.can_play = true;
innerFrame.device_state.has_can_play = true;
innerFrame.device_state.volume = configMan->volume;
innerFrame.device_state.has_volume = true;
innerFrame.device_state.name = strdup(configMan->deviceName.c_str());
innerFrame.state.track_count = 0;
// Prepare player's capabilities
addCapability(CapabilityType_kCanBePlayer, 1);
addCapability(CapabilityType_kDeviceType, 4);
addCapability(CapabilityType_kGaiaEqConnectId, 1);
addCapability(CapabilityType_kSupportsLogout, 0);
addCapability(CapabilityType_kIsObservable, 1);
addCapability(CapabilityType_kVolumeSteps, 64);
addCapability(CapabilityType_kSupportedContexts, -1,
std::vector<std::string>({"album", "playlist", "search", "inbox",
"toplist", "starred", "publishedstarred", "track"}));
addCapability(CapabilityType_kSupportedTypes, -1,
std::vector<std::string>({"audio/local", "audio/track", "audio/episode", "local", "track"}));
innerFrame.device_state.capabilities_count = 8;
}
PlayerState::~PlayerState() {
pb_release(Frame_fields, &innerFrame);
pb_release(Frame_fields, &remoteFrame);
}
void PlayerState::setPlaybackState(const PlaybackState state)
{
switch (state)
{
case PlaybackState::Loading:
// Prepare the playback at position 0
innerFrame.state.status = PlayStatus_kPlayStatusPause;
innerFrame.state.position_ms = 0;
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
break;
case PlaybackState::Playing:
innerFrame.state.status = PlayStatus_kPlayStatusPlay;
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
break;
case PlaybackState::Stopped:
break;
case PlaybackState::Paused:
// Update state and recalculate current song position
innerFrame.state.status = PlayStatus_kPlayStatusPause;
uint32_t diff = timeProvider->getSyncedTimestamp() - innerFrame.state.position_measured_at;
this->updatePositionMs(innerFrame.state.position_ms + diff);
break;
}
}
bool PlayerState::isActive()
{
return innerFrame.device_state.is_active;
}
bool PlayerState::nextTrack()
{
if (innerFrame.state.repeat) return true;
innerFrame.state.playing_track_index++;
if (innerFrame.state.playing_track_index >= innerFrame.state.track_count)
{
innerFrame.state.playing_track_index = 0;
if (!innerFrame.state.repeat)
{
setPlaybackState(PlaybackState::Paused);
return false;
}
}
return true;
}
void PlayerState::prevTrack()
{
if (innerFrame.state.playing_track_index > 0)
{
innerFrame.state.playing_track_index--;
}
else if (innerFrame.state.repeat)
{
innerFrame.state.playing_track_index = innerFrame.state.track_count - 1;
}
}
void PlayerState::setActive(bool isActive)
{
innerFrame.device_state.is_active = isActive;
if (isActive)
{
innerFrame.device_state.became_active_at = timeProvider->getSyncedTimestamp();
innerFrame.device_state.has_became_active_at = true;
}
}
void PlayerState::updatePositionMs(uint32_t position)
{
innerFrame.state.position_ms = position;
innerFrame.state.position_measured_at = timeProvider->getSyncedTimestamp();
}
#define FREE(ptr) { free(ptr); ptr = NULL; }
#define STRDUP(dst, src) if(src != NULL) { dst = strdup(src); } else { FREE(dst); } // strdup null pointer safe
void PlayerState::updateTracks()
{
CSPOT_LOG(info, "---- Track count %d", remoteFrame.state.track_count);
CSPOT_LOG(info, "---- Inner track count %d", innerFrame.state.track_count);
// free unused tracks
if(innerFrame.state.track_count > remoteFrame.state.track_count)
{
for(uint16_t i = remoteFrame.state.track_count; i < innerFrame.state.track_count; ++i)
{
FREE(innerFrame.state.track[i].gid);
FREE(innerFrame.state.track[i].uri);
FREE(innerFrame.state.track[i].context);
}
}
// reallocate memory for new tracks
innerFrame.state.track = (TrackRef *) realloc(innerFrame.state.track, sizeof(TrackRef) * remoteFrame.state.track_count);
for(uint16_t i = 0; i < remoteFrame.state.track_count; ++i)
{
if(i >= innerFrame.state.track_count) {
innerFrame.state.track[i].gid = NULL;
innerFrame.state.track[i].uri = NULL;
innerFrame.state.track[i].context = NULL;
}
if(remoteFrame.state.track[i].gid != NULL)
{
uint16_t gid_size = remoteFrame.state.track[i].gid->size;
innerFrame.state.track[i].gid = (pb_bytes_array_t *) realloc(innerFrame.state.track[i].gid, PB_BYTES_ARRAY_T_ALLOCSIZE(gid_size));
memcpy(innerFrame.state.track[i].gid->bytes, remoteFrame.state.track[i].gid->bytes, gid_size);
innerFrame.state.track[i].gid->size = gid_size;
}
innerFrame.state.track[i].has_queued = remoteFrame.state.track[i].has_queued;
innerFrame.state.track[i].queued = remoteFrame.state.track[i].queued;
STRDUP(innerFrame.state.track[i].uri, remoteFrame.state.track[i].uri);
STRDUP(innerFrame.state.track[i].context, remoteFrame.state.track[i].context);
}
innerFrame.state.context_uri = (char *) realloc(innerFrame.state.context_uri,
strlen(remoteFrame.state.context_uri) + 1);
strcpy(innerFrame.state.context_uri, remoteFrame.state.context_uri);
innerFrame.state.track_count = remoteFrame.state.track_count;
innerFrame.state.has_playing_track_index = true;
innerFrame.state.playing_track_index = remoteFrame.state.playing_track_index;
if (remoteFrame.state.repeat)
{
setRepeat(true);
}
if (remoteFrame.state.shuffle)
{
setShuffle(true);
}
}
void PlayerState::setVolume(uint32_t volume)
{
innerFrame.device_state.volume = volume;
configMan->volume = volume;
configMan->save();
}
void PlayerState::setShuffle(bool shuffle)
{
innerFrame.state.shuffle = shuffle;
if (shuffle)
{
// Put current song at the begining
std::swap(innerFrame.state.track[0], innerFrame.state.track[innerFrame.state.playing_track_index]);
// Shuffle current tracks
for (int x = 1; x < innerFrame.state.track_count - 1; x++)
{
auto j = x + (std::rand() % (innerFrame.state.track_count - x));
std::swap(innerFrame.state.track[j], innerFrame.state.track[x]);
}
innerFrame.state.playing_track_index = 0;
}
}
void PlayerState::setRepeat(bool repeat)
{
innerFrame.state.repeat = repeat;
}
std::shared_ptr<TrackReference> PlayerState::getCurrentTrack()
{
// Wrap current track in a class
return std::make_shared<TrackReference>(&innerFrame.state.track[innerFrame.state.playing_track_index]);
}
std::vector<uint8_t> PlayerState::encodeCurrentFrame(MessageType typ)
{
free(innerFrame.ident);
free(innerFrame.protocol_version);
// Prepare current frame info
innerFrame.version = 1;
innerFrame.ident = strdup(deviceId);
innerFrame.seq_nr = this->seqNum;
innerFrame.protocol_version = strdup(protocolVersion);
innerFrame.typ = typ;
innerFrame.state_update_id = timeProvider->getSyncedTimestamp();
innerFrame.has_version = true;
innerFrame.has_seq_nr = true;
innerFrame.recipient_count = 0;
innerFrame.has_state = true;
innerFrame.has_device_state = true;
innerFrame.has_typ = true;
innerFrame.has_state_update_id = true;
this->seqNum += 1;
return pbEncode(Frame_fields, &innerFrame);
}
// Wraps messy nanopb setters. @TODO: find a better way to handle this
void PlayerState::addCapability(CapabilityType typ, int intValue, std::vector<std::string> stringValue)
{
innerFrame.device_state.capabilities[capabilityIndex].has_typ = true;
this->innerFrame.device_state.capabilities[capabilityIndex].typ = typ;
if (intValue != -1)
{
this->innerFrame.device_state.capabilities[capabilityIndex].intValue[0] = intValue;
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count = 1;
}
else
{
this->innerFrame.device_state.capabilities[capabilityIndex].intValue_count = 0;
}
for (int x = 0; x < stringValue.size(); x++)
{
pbPutString(stringValue[x], this->innerFrame.device_state.capabilities[capabilityIndex].stringValue[x]);
}
this->innerFrame.device_state.capabilities[capabilityIndex].stringValue_count = stringValue.size();
this->capabilityIndex += 1;
}

View File

@@ -1,189 +1,85 @@
#include "Session.h"
#include "MercuryManager.h"
#include "Logger.h"
#include <memory>
#include "AuthChallenges.h"
using random_bytes_engine = std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
using random_bytes_engine =
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
Session::Session()
{
this->clientHello = {};
this->apResponse = {};
this->authRequest = {};
this->clientResPlaintext = {};
using namespace cspot;
// Generates the public and priv key
this->crypto = std::make_unique<Crypto>();
this->shanConn = std::make_shared<ShannonConnection>();
Session::Session() {
this->challenges = std::make_unique<cspot::AuthChallenges>();
}
Session::~Session()
{
pb_release(ClientHello_fields, &clientHello);
pb_release(APResponseMessage_fields, &apResponse);
pb_release(ClientResponsePlaintext_fields, &clientResPlaintext);
Session::~Session() {}
void Session::connect(std::unique_ptr<cspot::PlainConnection> connection) {
this->conn = std::move(connection);
conn->timeoutHandler = [this]() {
return this->triggerTimeout();
};
auto helloPacket = this->conn->sendPrefixPacket(
{0x00, 0x04}, this->challenges->prepareClientHello());
auto apResponse = this->conn->recvPacket();
CSPOT_LOG(info, "Received APHello response");
auto solvedHello = this->challenges->solveApHello(helloPacket, apResponse);
conn->sendPrefixPacket({}, solvedHello);
CSPOT_LOG(debug, "Received shannon keys");
// Generates the public and priv key
this->shanConn = std::make_shared<ShannonConnection>();
// Init shanno-encrypted connection
this->shanConn->wrapConnection(this->conn, challenges->shanSendKey,
challenges->shanRecvKey);
}
void Session::connect(std::unique_ptr<PlainConnection> connection)
{
this->conn = std::move(connection);
auto helloPacket = this->sendClientHelloRequest();
this->processAPHelloResponse(helloPacket);
void Session::connectWithRandomAp() {
auto apResolver = std::make_unique<ApResolve>("");
auto conn = std::make_unique<cspot::PlainConnection>();
conn->timeoutHandler = [this]() {
return this->triggerTimeout();
};
auto apAddr = apResolver->fetchFirstApAddress();
CSPOT_LOG(debug, "Connecting with AP <%s>", apAddr.c_str());
conn->connect(apAddr);
this->connect(std::move(conn));
}
void Session::connectWithRandomAp()
{
auto apResolver = std::make_unique<ApResolve>();
this->conn = std::make_unique<PlainConnection>();
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob) {
// save auth blob for reconnection purposes
authBlob = blob;
// prepare authentication request proto
auto data = challenges->prepareAuthPacket(blob->authData, blob->authType,
deviceId, blob->username);
auto apAddr = apResolver->fetchFirstApAddress();
CSPOT_LOG(debug, "Connecting with AP <%s>", apAddr.c_str());
this->conn->connectToAp(apAddr);
auto helloPacket = this->sendClientHelloRequest();
CSPOT_LOG(debug, "Sending APHello packet...");
this->processAPHelloResponse(helloPacket);
}
// Send login request
this->shanConn->sendPacket(LOGIN_REQUEST_COMMAND, data);
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob)
{
// save auth blob for reconnection purposes
authBlob = blob;
// prepare authentication request proto
pbPutString(blob->username, authRequest.login_credentials.username);
std::copy(blob->authData.begin(), blob->authData.end(), authRequest.login_credentials.auth_data.bytes);
authRequest.login_credentials.auth_data.size = blob->authData.size();
authRequest.login_credentials.typ = (AuthenticationType) blob->authType;
authRequest.system_info.cpu_family = CpuFamily_CPU_UNKNOWN;
authRequest.system_info.os = Os_OS_UNKNOWN;
auto infoStr = std::string(informationString);
pbPutString(infoStr, authRequest.system_info.system_information_string);
auto deviceIdStr = std::string(deviceId);
pbPutString(deviceId, authRequest.system_info.device_id);
auto versionStr = std::string(versionString);
pbPutString(versionStr, authRequest.version_string);
authRequest.has_version_string = true;
auto data = pbEncode(ClientResponseEncrypted_fields, &authRequest);
// Send login request
this->shanConn->sendPacket(LOGIN_REQUEST_COMMAND, data);
auto packet = this->shanConn->recvPacket();
switch (packet->command)
{
case AUTH_SUCCESSFUL_COMMAND:
{
CSPOT_LOG(debug, "Authorization successful");
// @TODO store the reusable credentials
// PBWrapper<APWelcome> welcomePacket(packet->data)
return std::vector<uint8_t>({0x1}); // TODO: return actual reusable credentaials to be stored somewhere
break;
auto packet = this->shanConn->recvPacket();
switch (packet.command) {
case AUTH_SUCCESSFUL_COMMAND: {
CSPOT_LOG(debug, "Authorization successful");
return std::vector<uint8_t>(
{0x1}); // TODO: return actual reusable credentaials to be stored somewhere
break;
}
case AUTH_DECLINED_COMMAND:
{
CSPOT_LOG(error, "Authorization declined");
break;
case AUTH_DECLINED_COMMAND: {
CSPOT_LOG(error, "Authorization declined");
break;
}
default:
CSPOT_LOG(error, "Unknown auth fail code %d", packet->command);
}
CSPOT_LOG(error, "Unknown auth fail code %d", packet.command);
}
return std::vector<uint8_t>(0);
}
void Session::processAPHelloResponse(std::vector<uint8_t> &helloPacket)
{
CSPOT_LOG(debug, "Processing AP hello response...");
auto data = this->conn->recvPacket();
CSPOT_LOG(debug, "Received AP hello response");
// Decode the response
auto skipSize = std::vector<uint8_t>(data.begin() + 4, data.end());
pb_release(APResponseMessage_fields, &apResponse);
pbDecode(apResponse, APResponseMessage_fields, skipSize);
auto diffieKey = std::vector<uint8_t>(apResponse.challenge.login_crypto_challenge.diffie_hellman.gs, apResponse.challenge.login_crypto_challenge.diffie_hellman.gs + 96);
// Compute the diffie hellman shared key based on the response
auto sharedKey = this->crypto->dhCalculateShared(diffieKey);
// Init client packet + Init server packets are required for the hmac challenge
data.insert(data.begin(), helloPacket.begin(), helloPacket.end());
// Solve the hmac challenge
auto resultData = std::vector<uint8_t>(0);
for (int x = 1; x < 6; x++)
{
auto challengeVector = std::vector<uint8_t>(1);
challengeVector[0] = x;
challengeVector.insert(challengeVector.begin(), data.begin(), data.end());
auto digest = crypto->sha1HMAC(sharedKey, challengeVector);
resultData.insert(resultData.end(), digest.begin(), digest.end());
}
auto lastVec = std::vector<uint8_t>(resultData.begin(), resultData.begin() + 0x14);
// Digest generated!
auto digest = crypto->sha1HMAC(lastVec, data);
clientResPlaintext.login_crypto_response.has_diffie_hellman = true;
std::copy(digest.begin(),
digest.end(),
clientResPlaintext.login_crypto_response.diffie_hellman.hmac);
auto resultPacket = pbEncode(ClientResponsePlaintext_fields, &clientResPlaintext);
auto emptyPrefix = std::vector<uint8_t>(0);
this->conn->sendPrefixPacket(emptyPrefix, resultPacket);
// Get send and receive keys
auto sendKey = std::vector<uint8_t>(resultData.begin() + 0x14, resultData.begin() + 0x34);
auto recvKey = std::vector<uint8_t>(resultData.begin() + 0x34, resultData.begin() + 0x54);
CSPOT_LOG(debug, "Received shannon keys");
// Init shanno-encrypted connection
this->shanConn->wrapConnection(this->conn, sendKey, recvKey);
return std::vector<uint8_t>(0);
}
void Session::close() {
this->conn->closeSocket();
}
std::vector<uint8_t> Session::sendClientHelloRequest()
{
// Prepare protobuf message
this->crypto->dhInit();
// Copy the public key into diffiehellman hello packet
std::copy(this->crypto->publicKey.begin(),
this->crypto->publicKey.end(),
clientHello.login_crypto_hello.diffie_hellman.gc);
clientHello.login_crypto_hello.diffie_hellman.server_keys_known = 1;
clientHello.build_info.product = Product_PRODUCT_CLIENT;
clientHello.build_info.platform = Platform2_PLATFORM_LINUX_X86;
clientHello.build_info.version = SPOTIFY_VERSION;
clientHello.feature_set.autoupdate2 = true;
clientHello.cryptosuites_supported[0] = Cryptosuite_CRYPTO_SUITE_SHANNON;
clientHello.padding[0] = 0x1E;
clientHello.has_feature_set = true;
clientHello.login_crypto_hello.has_diffie_hellman = true;
clientHello.has_padding = true;
clientHello.has_feature_set = true;
// Generate the random nonce
auto nonce = crypto->generateVectorWithRandomData(16);
std::copy(nonce.begin(), nonce.end(), clientHello.client_nonce);
auto vecData = pbEncode(ClientHello_fields, &clientHello);
auto prefix = std::vector<uint8_t>({0x00, 0x04});
return this->conn->sendPrefixPacket(prefix, vecData);
this->conn->close();
}

View File

@@ -9,16 +9,18 @@ using std::size_t;
static inline uint32_t rotl(uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
// assert ( (c<=mask) &&"rotate by type width or more");
c &= sizeof(n) * CHAR_BIT - 1;
return (n << c) | (n >> (sizeof(n)*CHAR_BIT-c));
c &= mask;
return (n << c) | (n >> ((-c) & mask));
}
static inline uint32_t rotr(uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT * sizeof(n) - 1); // assumes width is a power of 2.
// assert ( (c<=mask) &&"rotate by type width or more");
c &= sizeof(n) * CHAR_BIT - 1;
return (n >> c) | (n << (sizeof(n)*CHAR_BIT-c));
c &= mask;
return (n >> c) | (n << ((-c) & mask));
}
uint32_t Shannon::sbox1(uint32_t w)
@@ -140,7 +142,7 @@ void Shannon::diffuse()
#define ADDKEY(k) \
this->R[KEYP] ^= (k);
void Shannon::loadKey(const std::vector<uint8_t> &key)
void Shannon::loadKey(const std::vector<uint8_t>& key)
{
int i, j;
uint32_t k;
@@ -182,7 +184,7 @@ void Shannon::loadKey(const std::vector<uint8_t> &key)
this->R[i] ^= this->CRC[i];
}
void Shannon::key(const std::vector<uint8_t> &key)
void Shannon::key(const std::vector<uint8_t>& key)
{
this->initState();
this->loadKey(key);
@@ -191,7 +193,7 @@ void Shannon::key(const std::vector<uint8_t> &key)
this->nbuf = 0;
}
void Shannon::nonce(const std::vector<uint8_t> &nonce)
void Shannon::nonce(const std::vector<uint8_t>& nonce)
{
this->reloadState();
this->konst = Shannon::INITKONST;
@@ -200,11 +202,11 @@ void Shannon::nonce(const std::vector<uint8_t> &nonce)
this->nbuf = 0;
}
void Shannon::stream(std::vector<uint8_t> &bufVec)
void Shannon::stream(std::vector<uint8_t>& bufVec)
{
uint8_t *endbuf;
uint8_t* endbuf;
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t* buf = bufVec.data();
/* handle any previously buffered bytes */
while (this->nbuf != 0 && nbytes != 0)
{
@@ -239,12 +241,12 @@ void Shannon::stream(std::vector<uint8_t> &bufVec)
}
}
void Shannon::maconly(std::vector<uint8_t> &bufVec)
void Shannon::maconly(std::vector<uint8_t>& bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t* buf = bufVec.data();
uint8_t *endbuf;
uint8_t* endbuf;
/* handle any previously buffered bytes */
if (this->nbuf != 0)
@@ -286,11 +288,11 @@ void Shannon::maconly(std::vector<uint8_t> &bufVec)
}
}
void Shannon::encrypt(std::vector<uint8_t> &bufVec)
void Shannon::encrypt(std::vector<uint8_t>& bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t *endbuf;
uint8_t* buf = bufVec.data();
uint8_t* endbuf;
uint32_t t = 0;
/* handle any previously buffered bytes */
@@ -341,11 +343,11 @@ void Shannon::encrypt(std::vector<uint8_t> &bufVec)
}
void Shannon::decrypt(std::vector<uint8_t> &bufVec)
void Shannon::decrypt(std::vector<uint8_t>& bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t *endbuf;
uint8_t* buf = bufVec.data();
uint8_t* endbuf;
uint32_t t = 0;
/* handle any previously buffered bytes */
@@ -394,10 +396,10 @@ void Shannon::decrypt(std::vector<uint8_t> &bufVec)
}
}
void Shannon::finish(std::vector<uint8_t> &bufVec)
void Shannon::finish(std::vector<uint8_t>& bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t* buf = bufVec.data();
int i;
/* handle any previously buffered bytes */
@@ -408,10 +410,10 @@ void Shannon::finish(std::vector<uint8_t> &bufVec)
}
/* perturb the MAC to mark end of input.
* Note that only the stream register is updated, not the CRC. This is an
* action that can't be duplicated by passing in plaintext, hence
* defeating any kind of extension attack.
*/
* Note that only the stream register is updated, not the CRC. This is an
* action that can't be duplicated by passing in plaintext, hence
* defeating any kind of extension attack.
*/
this->cycle();
ADDKEY(INITKONST ^ (this->nbuf << 3));
this->nbuf = 0;

View File

@@ -1,100 +1,96 @@
#include "ShannonConnection.h"
#include "Logger.h"
#include "Packet.h"
ShannonConnection::ShannonConnection()
{
using namespace cspot;
ShannonConnection::ShannonConnection() {}
ShannonConnection::~ShannonConnection() {}
void ShannonConnection::wrapConnection(
std::shared_ptr<cspot::PlainConnection> conn, std::vector<uint8_t>& sendKey,
std::vector<uint8_t>& recvKey) {
this->conn = conn;
this->sendCipher = std::make_unique<Shannon>();
this->recvCipher = std::make_unique<Shannon>();
// Set keys
this->sendCipher->key(sendKey);
this->recvCipher->key(recvKey);
// Set initial nonce
this->sendCipher->nonce(pack<uint32_t>(htonl(0)));
this->recvCipher->nonce(pack<uint32_t>(htonl(0)));
}
ShannonConnection::~ShannonConnection()
{
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t>& data) {
std::scoped_lock lock(this->writeMutex);
auto rawPacket = this->cipherPacket(cmd, data);
// Shannon encrypt the packet and write it to sock
this->sendCipher->encrypt(rawPacket);
this->conn->writeBlock(rawPacket);
// Generate mac
std::vector<uint8_t> mac(MAC_SIZE);
this->sendCipher->finish(mac);
// Update the nonce
this->sendNonce += 1;
this->sendCipher->nonce(pack<uint32_t>(htonl(this->sendNonce)));
// Write the mac to sock
this->conn->writeBlock(mac);
}
void ShannonConnection::wrapConnection(std::shared_ptr<PlainConnection> conn, std::vector<uint8_t> &sendKey, std::vector<uint8_t> &recvKey)
{
this->conn = conn;
cspot::Packet ShannonConnection::recvPacket() {
std::scoped_lock lock(this->readMutex);
this->sendCipher = std::make_unique<Shannon>();
this->recvCipher = std::make_unique<Shannon>();
std::vector<uint8_t> data(3);
// Receive 3 bytes, cmd + int16 size
this->conn->readBlock(data.data(), 3);
this->recvCipher->decrypt(data);
// Set keys
this->sendCipher->key(sendKey);
this->recvCipher->key(recvKey);
auto readSize = ntohs(extract<uint16_t>(data, 1));
auto packetData = std::vector<uint8_t>(readSize);
// Set initial nonce
this->sendCipher->nonce(pack<uint32_t>(htonl(0)));
this->recvCipher->nonce(pack<uint32_t>(htonl(0)));
// Read and decode if the packet has an actual body
if (readSize > 0) {
this->conn->readBlock(packetData.data(), readSize);
this->recvCipher->decrypt(packetData);
}
// Read mac
std::vector<uint8_t> mac(MAC_SIZE);
this->conn->readBlock(mac.data(), MAC_SIZE);
// Generate mac
std::vector<uint8_t> mac2(MAC_SIZE);
this->recvCipher->finish(mac2);
if (mac != mac2) {
CSPOT_LOG(error, "Shannon read: Mac doesn't match");
}
// Update the nonce
this->recvNonce += 1;
this->recvCipher->nonce(pack<uint32_t>(htonl(this->recvNonce)));
uint8_t cmd = 0;
if (data.size() > 0) {
cmd = data[0];
}
// data[0] == cmd
return Packet{cmd, packetData};
}
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t> &data)
{
this->writeMutex.lock();
auto rawPacket = this->cipherPacket(cmd, data);
std::vector<uint8_t> ShannonConnection::cipherPacket(
uint8_t cmd, std::vector<uint8_t>& data) {
// Generate packet structure, [Command] [Size] [Raw data]
auto sizeRaw = pack<uint16_t>(htons(uint16_t(data.size())));
// Shannon encrypt the packet and write it to sock
this->sendCipher->encrypt(rawPacket);
this->conn->writeBlock(rawPacket);
sizeRaw.insert(sizeRaw.begin(), cmd);
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
// Generate mac
std::vector<uint8_t> mac(MAC_SIZE);
this->sendCipher->finish(mac);
// Update the nonce
this->sendNonce += 1;
this->sendCipher->nonce(pack<uint32_t>(htonl(this->sendNonce)));
// Write the mac to sock
this->conn->writeBlock(mac);
this->writeMutex.unlock();
}
std::unique_ptr<Packet> ShannonConnection::recvPacket()
{
this->readMutex.lock();
// Receive 3 bytes, cmd + int16 size
auto data = this->conn->readBlock(3);
this->recvCipher->decrypt(data);
auto packetData = std::vector<uint8_t>();
auto readSize = ntohs(extract<uint16_t>(data, 1));
// Read and decode if the packet has an actual body
if (readSize > 0)
{
packetData = this->conn->readBlock(readSize);
this->recvCipher->decrypt(packetData);
}
// Read mac
auto mac = this->conn->readBlock(MAC_SIZE);
// Generate mac
std::vector<uint8_t> mac2(MAC_SIZE);
this->recvCipher->finish(mac2);
if (mac != mac2)
{
CSPOT_LOG(error, "Shannon read: Mac doesn't match");
}
// Update the nonce
this->recvNonce += 1;
this->recvCipher->nonce(pack<uint32_t>(htonl(this->recvNonce)));
// Unlock the mutex
this->readMutex.unlock();
// data[0] == cmd
return std::make_unique<Packet>(data[0], packetData);
}
std::vector<uint8_t> ShannonConnection::cipherPacket(uint8_t cmd, std::vector<uint8_t> &data)
{
// Generate packet structure, [Command] [Size] [Raw data]
auto sizeRaw = pack<uint16_t>(htons(uint16_t(data.size())));
sizeRaw.insert(sizeRaw.begin(), cmd);
sizeRaw.insert(sizeRaw.end(), data.begin(), data.end());
return sizeRaw;
return sizeRaw;
}

View File

@@ -1,244 +0,0 @@
#include "SpircController.h"
#include "ConfigJSON.h"
#include "Logger.h"
#include "SpotifyTrack.h"
SpircController::SpircController(std::shared_ptr<MercuryManager> manager,
std::string username,
std::shared_ptr<AudioSink> audioSink) {
this->manager = manager;
this->player = std::make_unique<Player>(manager, audioSink);
this->state = std::make_unique<PlayerState>(manager->timeProvider);
this->username = username;
player->endOfFileCallback = [=]() {
if (state->nextTrack()) {
loadTrack();
}
};
player->setVolume(configMan->volume);
subscribe();
}
SpircController::~SpircController() {
}
void SpircController::subscribe() {
mercuryCallback responseLambda = [=](std::unique_ptr<MercuryResponse> res) {
// this->trackInformationCallback(std::move(res));
sendCmd(MessageType_kMessageTypeHello);
CSPOT_LOG(debug, "Sent kMessageTypeHello!");
};
mercuryCallback subLambda = [=](std::unique_ptr<MercuryResponse> res) {
this->handleFrame(res->parts[0]);
};
manager->execute(MercuryType::SUB,
"hm://remote/user/" + this->username + "/", responseLambda,
subLambda);
}
void SpircController::setPause(bool isPaused, bool notifyPlayer) {
sendEvent(CSpotEventType::PLAY_PAUSE, isPaused);
if (isPaused) {
CSPOT_LOG(debug, "External pause command");
if (notifyPlayer) player->pause();
state->setPlaybackState(PlaybackState::Paused);
} else {
CSPOT_LOG(debug, "External play command");
if (notifyPlayer) player->play();
state->setPlaybackState(PlaybackState::Playing);
}
notify();
}
void SpircController::disconnect(void) {
player->cancelCurrentTrack();
state->setActive(false);
notify();
// Send the event at the end at it might be a last gasp
sendEvent(CSpotEventType::DISC);
}
void SpircController::playToggle() {
if (state->innerFrame.state.status == PlayStatus_kPlayStatusPause) {
setPause(false);
} else {
setPause(true);
}
}
void SpircController::adjustVolume(int by) {
if (state->innerFrame.device_state.has_volume) {
int volume = state->innerFrame.device_state.volume + by;
if (volume < 0) volume = 0;
else if (volume > MAX_VOLUME) volume = MAX_VOLUME;
setVolume(volume);
}
}
void SpircController::setVolume(int volume) {
setRemoteVolume(volume);
player->setVolume(volume);
configMan->save();
}
void SpircController::setRemoteVolume(int volume) {
state->setVolume(volume);
notify();
}
void SpircController::nextSong() {
if (state->nextTrack()) {
loadTrack();
} else {
player->cancelCurrentTrack();
}
notify();
}
void SpircController::prevSong() {
state->prevTrack();
loadTrack();
notify();
}
void SpircController::handleFrame(std::vector<uint8_t> &data) {
pb_release(Frame_fields, &state->remoteFrame);
pbDecode(state->remoteFrame, Frame_fields, data);
//CSPOT_LOG(info, "FRAME RECEIVED %d", (int) state->remoteFrame.typ);
switch (state->remoteFrame.typ) {
case MessageType_kMessageTypeNotify: {
CSPOT_LOG(debug, "Notify frame");
// Pause the playback if another player took control
if (state->isActive() &&
state->remoteFrame.device_state.is_active) {
disconnect();
}
break;
}
case MessageType_kMessageTypeSeek: {
CSPOT_LOG(debug, "Seek command");
sendEvent(CSpotEventType::SEEK, (int) state->remoteFrame.position);
state->updatePositionMs(state->remoteFrame.position);
this->player->seekMs(state->remoteFrame.position);
notify();
break;
}
case MessageType_kMessageTypeVolume:
sendEvent(CSpotEventType::VOLUME, (int) state->remoteFrame.volume);
setVolume(state->remoteFrame.volume);
break;
case MessageType_kMessageTypePause:
setPause(true);
break;
case MessageType_kMessageTypePlay:
setPause(false);
break;
case MessageType_kMessageTypeNext:
sendEvent(CSpotEventType::NEXT);
nextSong();
break;
case MessageType_kMessageTypePrev:
sendEvent(CSpotEventType::PREV);
prevSong();
break;
case MessageType_kMessageTypeLoad: {
CSPOT_LOG(debug, "Load frame!");
state->setActive(true);
// Every sane person on the planet would expect std::move to work here.
// And it does... on every single platform EXCEPT for ESP32 for some
// reason. For which it corrupts memory and makes printf fail. so yeah.
// its cursed.
state->updateTracks();
// bool isPaused = (state->remoteFrame.state->status.value() ==
// PlayStatus::kPlayStatusPlay) ? false : true;
loadTrack(state->remoteFrame.state.position_ms, false);
state->updatePositionMs(state->remoteFrame.state.position_ms);
this->notify();
break;
}
case MessageType_kMessageTypeReplace: {
CSPOT_LOG(debug, "Got replace frame!");
state->updateTracks();
this->notify();
break;
}
case MessageType_kMessageTypeShuffle: {
CSPOT_LOG(debug, "Got shuffle frame");
state->setShuffle(state->remoteFrame.state.shuffle);
this->notify();
break;
}
case MessageType_kMessageTypeRepeat: {
CSPOT_LOG(debug, "Got repeat frame");
state->setRepeat(state->remoteFrame.state.repeat);
this->notify();
break;
}
default:
break;
}
}
void SpircController::loadTrack(uint32_t position_ms, bool isPaused) {
sendEvent(CSpotEventType::LOAD, (int) position_ms);
state->setPlaybackState(PlaybackState::Loading);
std::function<void(bool)> loadedLambda = [=](bool needFlush) {
// Loading finished, notify that playback started
setPause(isPaused, false);
sendEvent(CSpotEventType::PLAYBACK_START, needFlush);
};
player->handleLoad(state->getCurrentTrack(), loadedLambda, position_ms,
isPaused);
}
void SpircController::notify() {
this->sendCmd(MessageType_kMessageTypeNotify);
}
void SpircController::sendEvent(CSpotEventType eventType, std::variant<TrackInfo, int, bool> data) {
if (eventHandler != nullptr) {
CSpotEvent event = {
.eventType = eventType,
.data = data,
};
eventHandler(event);
}
}
void SpircController::setEventHandler(cspotEventHandler callback) {
this->eventHandler = callback;
player->trackChanged = ([this](TrackInfo &track) {
TrackInfo info;
info.album = track.album;
info.artist = track.artist;
info.imageUrl = track.imageUrl;
info.name = track.name;
info.duration = track.duration;
this->sendEvent(CSpotEventType::TRACK_INFO, info);
});
}
void SpircController::stopPlayer() { this->player->stop(); }
void SpircController::sendCmd(MessageType typ) {
// Serialize current player state
auto encodedFrame = state->encodeCurrentFrame(typ);
mercuryCallback responseLambda = [=](std::unique_ptr<MercuryResponse> res) {
};
auto parts = mercuryParts({encodedFrame});
this->manager->execute(MercuryType::SEND,
"hm://remote/user/" + this->username + "/",
responseLambda, parts);
}

View File

@@ -0,0 +1,319 @@
#include "SpircHandler.h"
#include <memory>
#include "AccessKeyFetcher.h"
#include "BellUtils.h"
#include "CSpotContext.h"
#include "Logger.h"
#include "MercurySession.h"
#include "PlaybackState.h"
#include "TrackPlayer.h"
#include "TrackReference.h"
#include "protobuf/spirc.pb.h"
using namespace cspot;
SpircHandler::SpircHandler(std::shared_ptr<cspot::Context> ctx)
: playbackState(ctx) {
auto isAiringCallback = [this]() {
return !(isNextTrackPreloaded || isRequestedFromLoad);
};
auto EOFCallback = [this]() {
auto ref = this->playbackState.getNextTrackRef();
if (!isNextTrackPreloaded && !isRequestedFromLoad && ref != nullptr) {
isNextTrackPreloaded = true;
auto trackRef = TrackReference::fromTrackRef(ref);
this->trackPlayer->loadTrackFromRef(trackRef, 0, true);
}
if (ref == nullptr) {
sendEvent(EventType::DEPLETED);
}
};
auto trackLoadedCallback = [this]() {
this->currentTrackInfo = this->trackPlayer->getCurrentTrackInfo();
if (isRequestedFromLoad) {
sendEvent(EventType::PLAYBACK_START, (int)nextTrackPosition);
setPause(false);
}
};
this->ctx = ctx;
this->trackPlayer = std::make_shared<TrackPlayer>(ctx, isAiringCallback, EOFCallback, trackLoadedCallback);
// Subscribe to mercury on session ready
ctx->session->setConnectedHandler([this]() { this->subscribeToMercury(); });
}
void SpircHandler::subscribeToMercury() {
auto responseLambda = [this](MercurySession::Response& res) {
if (res.fail)
return;
sendCmd(MessageType_kMessageTypeHello);
CSPOT_LOG(debug, "Sent kMessageTypeHello!");
// Assign country code
this->ctx->config.countryCode = this->ctx->session->getCountryCode();
};
auto subscriptionLambda = [this](MercurySession::Response& res) {
if (res.fail)
return;
CSPOT_LOG(debug, "Received subscription response");
this->handleFrame(res.parts[0]);
};
ctx->session->executeSubscription(
MercurySession::RequestType::SUB,
"hm://remote/user/" + ctx->config.username + "/", responseLambda,
subscriptionLambda);
}
void SpircHandler::loadTrackFromURI(const std::string& uri) {
// {track/episode}:{gid}
bool isEpisode = uri.find("episode:") != std::string::npos;
auto gid = stringHexToBytes(uri.substr(uri.find(":") + 1));
auto trackRef = TrackReference::fromGID(gid, isEpisode);
isRequestedFromLoad = true;
isNextTrackPreloaded = false;
playbackState.setActive(true);
auto playbackRef = playbackState.getCurrentTrackRef();
if (playbackRef != nullptr) {
playbackState.updatePositionMs(playbackState.remoteFrame.state.position_ms);
auto ref = TrackReference::fromTrackRef(playbackRef);
this->trackPlayer->loadTrackFromRef(
ref, playbackState.remoteFrame.state.position_ms, true);
playbackState.setPlaybackState(PlaybackState::State::Loading);
this->nextTrackPosition = playbackState.remoteFrame.state.position_ms;
}
this->notify();
}
void SpircHandler::notifyAudioReachedPlayback() {
if (isRequestedFromLoad || isNextTrackPreloaded) {
playbackState.updatePositionMs(nextTrackPosition);
playbackState.setPlaybackState(PlaybackState::State::Playing);
} else {
setPause(true);
}
isRequestedFromLoad = false;
if (isNextTrackPreloaded) {
isNextTrackPreloaded = false;
playbackState.nextTrack();
nextTrackPosition = 0;
}
this->notify();
sendEvent(EventType::TRACK_INFO, this->trackPlayer->getCurrentTrackInfo());
}
void SpircHandler::updatePositionMs(uint32_t position) {
playbackState.updatePositionMs(position);
notify();
}
void SpircHandler::disconnect() {
this->trackPlayer->stopTrack();
this->ctx->session->disconnect();
}
void SpircHandler::handleFrame(std::vector<uint8_t>& data) {
pb_release(Frame_fields, &playbackState.remoteFrame);
pbDecode(playbackState.remoteFrame, Frame_fields, data);
switch (playbackState.remoteFrame.typ) {
case MessageType_kMessageTypeNotify: {
CSPOT_LOG(debug, "Notify frame");
// Pause the playback if another player took control
if (playbackState.isActive() &&
playbackState.remoteFrame.device_state.is_active) {
CSPOT_LOG(debug, "Another player took control, pausing playback");
playbackState.setActive(false);
this->trackPlayer->stopTrack();
sendEvent(EventType::DISC);
}
break;
}
case MessageType_kMessageTypeSeek: {
/* If next track is already downloading, we can't seek in the current one anymore. Also,
* when last track has been reached, we has to restart as we can't tell the difference */
if ((!isNextTrackPreloaded && this->playbackState.getNextTrackRef()) || isRequestedFromLoad) {
CSPOT_LOG(debug, "Seek command while streaming current");
sendEvent(EventType::SEEK, (int)playbackState.remoteFrame.position);
playbackState.updatePositionMs(playbackState.remoteFrame.position);
trackPlayer->seekMs(playbackState.remoteFrame.position);
} else {
CSPOT_LOG(debug, "Seek command while streaming next or before started");
isRequestedFromLoad = true;
isNextTrackPreloaded = false;
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
this->trackPlayer->loadTrackFromRef(ref, playbackState.remoteFrame.position, true);
this->nextTrackPosition = playbackState.remoteFrame.position;
}
notify();
break;
}
case MessageType_kMessageTypeVolume:
playbackState.setVolume(playbackState.remoteFrame.volume);
this->notify();
sendEvent(EventType::VOLUME, (int)playbackState.remoteFrame.volume);
break;
case MessageType_kMessageTypePause:
setPause(true);
break;
case MessageType_kMessageTypePlay:
setPause(false);
break;
case MessageType_kMessageTypeNext:
nextSong();
sendEvent(EventType::NEXT);
break;
case MessageType_kMessageTypePrev:
previousSong();
sendEvent(EventType::PREV);
break;
case MessageType_kMessageTypeLoad: {
CSPOT_LOG(debug, "Load frame!");
isRequestedFromLoad = true;
isNextTrackPreloaded = false;
playbackState.setActive(true);
playbackState.updateTracks();
auto playbackRef = playbackState.getCurrentTrackRef();
if (playbackRef != nullptr) {
playbackState.updatePositionMs(
playbackState.remoteFrame.state.position_ms);
auto ref = TrackReference::fromTrackRef(playbackRef);
this->trackPlayer->loadTrackFromRef(
ref, playbackState.remoteFrame.state.position_ms, true);
playbackState.setPlaybackState(PlaybackState::State::Loading);
this->nextTrackPosition = playbackState.remoteFrame.state.position_ms;
}
this->notify();
break;
}
case MessageType_kMessageTypeReplace: {
CSPOT_LOG(debug, "Got replace frame");
playbackState.updateTracks();
this->notify();
break;
}
case MessageType_kMessageTypeShuffle: {
CSPOT_LOG(debug, "Got shuffle frame");
playbackState.setShuffle(playbackState.remoteFrame.state.shuffle);
this->notify();
break;
}
case MessageType_kMessageTypeRepeat: {
CSPOT_LOG(debug, "Got repeat frame");
playbackState.setRepeat(playbackState.remoteFrame.state.repeat);
this->notify();
break;
}
default:
break;
}
}
void SpircHandler::setRemoteVolume(int volume) {
playbackState.setVolume(volume);
notify();
}
void SpircHandler::notify() {
this->sendCmd(MessageType_kMessageTypeNotify);
}
void SpircHandler::nextSong() {
if (playbackState.nextTrack()) {
isRequestedFromLoad = true;
isNextTrackPreloaded = false;
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
this->trackPlayer->loadTrackFromRef(ref, 0, true);
} else {
sendEvent(EventType::FLUSH);
playbackState.updatePositionMs(0);
trackPlayer->stopTrack();
}
this->nextTrackPosition = 0;
notify();
}
void SpircHandler::previousSong() {
playbackState.prevTrack();
isRequestedFromLoad = true;
isNextTrackPreloaded = false;
sendEvent(EventType::PREV);
auto ref = TrackReference::fromTrackRef(playbackState.getCurrentTrackRef());
this->trackPlayer->loadTrackFromRef(ref, 0, true);
this->nextTrackPosition = 0;
notify();
}
std::shared_ptr<TrackPlayer> SpircHandler::getTrackPlayer() {
return this->trackPlayer;
}
void SpircHandler::sendCmd(MessageType typ) {
// Serialize current player state
auto encodedFrame = playbackState.encodeCurrentFrame(typ);
auto responseLambda = [=](MercurySession::Response& res) {
};
auto parts = MercurySession::DataParts({encodedFrame});
ctx->session->execute(MercurySession::RequestType::SEND,
"hm://remote/user/" + ctx->config.username + "/",
responseLambda, parts);
}
void SpircHandler::setEventHandler(EventHandler handler) {
this->eventHandler = handler;
}
void SpircHandler::setPause(bool isPaused) {
if (isPaused) {
CSPOT_LOG(debug, "External pause command");
playbackState.setPlaybackState(PlaybackState::State::Paused);
} else {
CSPOT_LOG(debug, "External play command");
playbackState.setPlaybackState(PlaybackState::State::Playing);
}
notify();
sendEvent(EventType::PLAY_PAUSE, isPaused);
}
void SpircHandler::sendEvent(EventType type) {
auto event = std::make_unique<Event>();
event->eventType = type;
event->data = {};
eventHandler(std::move(event));
}
void SpircHandler::sendEvent(EventType type, EventData data) {
auto event = std::make_unique<Event>();
event->eventType = type;
event->data = data;
eventHandler(std::move(event));
}

View File

@@ -1,229 +0,0 @@
#include "SpotifyTrack.h"
#ifndef _WIN32
#include "unistd.h"
#endif
#include "MercuryManager.h"
#include <cassert>
#include "CspotAssert.h"
#include "Logger.h"
#include "ConfigJSON.h"
SpotifyTrack::SpotifyTrack(std::shared_ptr<MercuryManager> manager, std::shared_ptr<TrackReference> trackReference, uint32_t position_ms, bool isPaused)
{
this->manager = manager;
this->fileId = std::vector<uint8_t>();
episodeInfo = {};
trackInfo = {};
mercuryCallback trackResponseLambda = [=](std::unique_ptr<MercuryResponse> res) {
this->trackInformationCallback(std::move(res), position_ms, isPaused);
};
mercuryCallback episodeResponseLambda = [=](std::unique_ptr<MercuryResponse> res) {
this->episodeInformationCallback(std::move(res), position_ms, isPaused);
};
if (trackReference->isEpisode)
{
this->reqSeqNum = this->manager->execute(MercuryType::GET, "hm://metadata/3/episode/" + bytesToHexString(trackReference->gid), episodeResponseLambda);
}
else
{
this->reqSeqNum = this->manager->execute(MercuryType::GET, "hm://metadata/3/track/" + bytesToHexString(trackReference->gid), trackResponseLambda);
}
}
SpotifyTrack::~SpotifyTrack()
{
this->manager->unregisterMercuryCallback(this->reqSeqNum);
this->manager->freeAudioKeyCallback();
pb_release(Track_fields, &this->trackInfo);
pb_release(Episode_fields, &this->episodeInfo);
}
bool SpotifyTrack::countryListContains(char *countryList, char *country)
{
uint16_t countryList_length = strlen(countryList);
for (int x = 0; x < countryList_length; x += 2)
{
if (countryList[x] == country[0] && countryList[x + 1] == country[1])
{
return true;
}
}
return false;
}
bool SpotifyTrack::canPlayTrack(int altIndex)
{
if(altIndex < 0)
{
for (int x = 0; x < trackInfo.restriction_count; x++)
{
if (trackInfo.restriction[x].countries_allowed != nullptr)
{
return countryListContains(trackInfo.restriction[x].countries_allowed, manager->countryCode);
}
if (trackInfo.restriction[x].countries_forbidden != nullptr)
{
return !countryListContains(trackInfo.restriction[x].countries_forbidden, manager->countryCode);
}
}
}
else
{
for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count; x++)
{
if (trackInfo.alternative[altIndex].restriction[x].countries_allowed != nullptr)
{
return countryListContains(trackInfo.alternative[altIndex].restriction[x].countries_allowed, manager->countryCode);
}
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden != nullptr)
{
return !countryListContains(trackInfo.alternative[altIndex].restriction[x].countries_forbidden, manager->countryCode);
}
}
}
return true;
}
void SpotifyTrack::trackInformationCallback(std::unique_ptr<MercuryResponse> response, uint32_t position_ms, bool isPaused)
{
if (this->fileId.size() != 0)
return;
CSPOT_ASSERT(response->parts.size() > 0, "response->parts.size() must be greater than 0");
pb_release(Track_fields, &trackInfo);
pbDecode(trackInfo, Track_fields, response->parts[0]);
CSPOT_LOG(info, "Track name: %s", trackInfo.name);
CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d", trackInfo.restriction_count);
int altIndex = -1;
while (!canPlayTrack(altIndex))
{
altIndex++;
CSPOT_LOG(info, "Trying alternative %d", altIndex);
if(altIndex >= trackInfo.alternative_count) {
// no alternatives for song
return;
}
}
std::vector<uint8_t> trackId;
this->fileId = std::vector<uint8_t>();
if(altIndex < 0)
{
trackId = pbArrayToVector(trackInfo.gid);
for (int x = 0; x < trackInfo.file_count; x++)
{
if (trackInfo.file[x].format == configMan->format)
{
this->fileId = pbArrayToVector(trackInfo.file[x].file_id);
break; // If file found stop searching
}
}
}
else
{
trackId = pbArrayToVector(trackInfo.alternative[altIndex].gid);
for (int x = 0; x < trackInfo.alternative[altIndex].file_count; x++)
{
if (trackInfo.alternative[altIndex].file[x].format == configMan->format)
{
this->fileId = pbArrayToVector(trackInfo.alternative[altIndex].file[x].file_id);
break; // If file found stop searching
}
}
}
if (trackInfoReceived != nullptr)
{
auto imageId = pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
TrackInfo simpleTrackInfo = {
.name = std::string(trackInfo.name),
.album = std::string(trackInfo.album.name),
.artist = std::string(trackInfo.artist[0].name),
.imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId),
.duration = trackInfo.duration,
};
trackInfoReceived(simpleTrackInfo);
}
this->requestAudioKey(this->fileId, trackId, trackInfo.duration, position_ms, isPaused);
}
void SpotifyTrack::episodeInformationCallback(std::unique_ptr<MercuryResponse> response, uint32_t position_ms, bool isPaused)
{
if (this->fileId.size() != 0)
return;
CSPOT_LOG(debug, "Got to episode");
CSPOT_ASSERT(response->parts.size() > 0, "response->parts.size() must be greater than 0");
pb_release(Episode_fields, &episodeInfo);
pbDecode(episodeInfo, Episode_fields, response->parts[0]);
CSPOT_LOG(info, "--- Episode name: %s", episodeInfo.name);
this->fileId = std::vector<uint8_t>();
// TODO: option to set file quality
for (int x = 0; x < episodeInfo.audio_count; x++)
{
if (episodeInfo.audio[x].format == AudioFormat_OGG_VORBIS_96)
{
this->fileId = pbArrayToVector(episodeInfo.audio[x].file_id);
break; // If file found stop searching
}
}
if (trackInfoReceived != nullptr)
{
auto imageId = pbArrayToVector(episodeInfo.covers->image[0].file_id);
TrackInfo simpleTrackInfo = {
.name = std::string(episodeInfo.name),
.album = "",
.artist = "",
.imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId),
.duration = trackInfo.duration,
};
trackInfoReceived(simpleTrackInfo);
}
this->requestAudioKey(pbArrayToVector(episodeInfo.gid), this->fileId, episodeInfo.duration, position_ms, isPaused);
}
void SpotifyTrack::requestAudioKey(std::vector<uint8_t> fileId, std::vector<uint8_t> trackId, int32_t trackDuration, uint32_t position_ms, bool isPaused)
{
audioKeyCallback audioKeyLambda = [=](bool success, std::vector<uint8_t> res) {
if (success)
{
CSPOT_LOG(info, "Successfully got audio key!");
auto audioKey = std::vector<uint8_t>(res.begin() + 4, res.end());
if (this->fileId.size() > 0)
{
this->audioStream = std::make_unique<ChunkedAudioStream>(this->fileId, audioKey, trackDuration, this->manager, position_ms, isPaused);
loadedTrackCallback();
}
else
{
CSPOT_LOG(error, "Error while fetching audiokey...");
}
}
else
{
auto code = ntohs(extract<uint16_t>(res, 4));
CSPOT_LOG(error, "Error while fetching audiokey, error code: %d", code);
}
};
this->manager->requestAudioKey(trackId, fileId, audioKeyLambda);
}

View File

@@ -1,10 +1,13 @@
#include "TimeProvider.h"
#include "Utils.h"
#include "Logger.h"
using namespace cspot;
TimeProvider::TimeProvider() {
}
void TimeProvider::syncWithPingPacket(const std::vector<uint8_t>& pongPacket) {
CSPOT_LOG(debug, "Time synced with spotify servers");
// Spotify's timestamp is in seconds since unix time - convert to millis.
uint64_t remoteTimestamp = ((uint64_t) ntohl(extract<uint32_t>(pongPacket, 0))) * 1000;
this->timestampDiff = remoteTimestamp - getCurrentTimestamp();

View File

@@ -0,0 +1,225 @@
#include "TrackPlayer.h"
#include <cstddef>
#include <fstream>
#include <memory>
#include <mutex>
#include <vector>
#include "CDNTrackStream.h"
#include "Logger.h"
#include "TrackReference.h"
using namespace cspot;
static size_t vorbisReadCb(void* ptr, size_t size, size_t nmemb,
TrackPlayer* self) {
return self->_vorbisRead(ptr, size, nmemb);
}
static int vorbisCloseCb(TrackPlayer* self) {
return self->_vorbisClose();
}
static int vorbisSeekCb(TrackPlayer* self, int64_t offset, int whence) {
return self->_vorbisSeek(offset, whence);
}
static long vorbisTellCb(TrackPlayer* self) {
return self->_vorbisTell();
}
TrackPlayer::TrackPlayer(std::shared_ptr<cspot::Context> ctx, isAiringCallback isAiring, EOFCallback eof, TrackLoadedCallback trackLoaded)
: bell::Task("cspot_player", 48 * 1024, 5, 1) {
this->ctx = ctx;
this->isAiring = isAiring;
this->eofCallback = eof;
this->trackLoaded = trackLoaded;
this->trackProvider = std::make_shared<cspot::TrackProvider>(ctx);
this->playbackSemaphore = std::make_unique<bell::WrappedSemaphore>(5);
// Initialize vorbis callbacks
vorbisFile = {};
vorbisCallbacks = {
(decltype(ov_callbacks::read_func))&vorbisReadCb,
(decltype(ov_callbacks::seek_func))&vorbisSeekCb,
(decltype(ov_callbacks::close_func))&vorbisCloseCb,
(decltype(ov_callbacks::tell_func))&vorbisTellCb,
};
isRunning = true;
startTask();
}
TrackPlayer::~TrackPlayer() {
isRunning = false;
std::scoped_lock lock(runningMutex);
}
void TrackPlayer::loadTrackFromRef(TrackReference& ref, size_t positionMs,
bool startAutomatically) {
this->playbackPosition = positionMs;
this->autoStart = startAutomatically;
auto nextTrack = trackProvider->loadFromTrackRef(ref);
stopTrack();
this->sequence++;
this->currentTrackStream = nextTrack;
this->playbackSemaphore->give();
}
void TrackPlayer::stopTrack() {
this->currentSongPlaying = false;
std::scoped_lock lock(playbackMutex);
}
void TrackPlayer::seekMs(size_t ms) {
std::scoped_lock lock(seekMutex);
#ifdef BELL_VORBIS_FLOAT
ov_time_seek(&vorbisFile, (double)ms / 1000);
#else
ov_time_seek(&vorbisFile, ms);
#endif
}
void TrackPlayer::runTask() {
std::scoped_lock lock(runningMutex);
while (isRunning) {
this->playbackSemaphore->twait(100);
if (this->currentTrackStream == nullptr) {
continue;
}
CSPOT_LOG(info, "Player received a track, waiting for it to be ready...");
// when track changed many times and very quickly, we are stuck on never-given semaphore
while (this->currentTrackStream->trackReady->twait(250));
CSPOT_LOG(info, "Got track");
if (this->currentTrackStream->status == CDNTrackStream::Status::FAILED) {
CSPOT_LOG(error, "Track failed to load, skipping it");
this->currentTrackStream = nullptr;
this->eofCallback();
continue;
}
this->currentSongPlaying = true;
this->trackLoaded();
this->playbackMutex.lock();
int32_t r = ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks);
if (playbackPosition > 0) {
#ifdef BELL_VORBIS_FLOAT
ov_time_seek(&vorbisFile, (double)playbackPosition / 1000);
#else
ov_time_seek(&vorbisFile, playbackPosition);
#endif
}
bool eof = false;
while (!eof && currentSongPlaying) {
seekMutex.lock();
#ifdef BELL_VORBIS_FLOAT
long ret = ov_read(&vorbisFile, (char*)&pcmBuffer[0], pcmBuffer.size(),
0, 2, 1, &currentSection);
#else
long ret = ov_read(&vorbisFile, (char*)&pcmBuffer[0], pcmBuffer.size(),
&currentSection);
#endif
seekMutex.unlock();
if (ret == 0) {
CSPOT_LOG(info, "EOF");
// and done :)
eof = true;
} else if (ret < 0) {
CSPOT_LOG(error, "An error has occured in the stream %d", ret);
currentSongPlaying = false;
} else {
if (this->dataCallback != nullptr) {
auto toWrite = ret;
while (!eof && currentSongPlaying && toWrite > 0) {
auto written =
dataCallback(pcmBuffer.data() + (ret - toWrite), toWrite,
this->currentTrackStream->trackInfo.trackId, this->sequence);
if (written == 0) {
BELL_SLEEP_MS(10);
}
toWrite -= written;
}
}
}
}
ov_clear(&vorbisFile);
// With very large buffers, track N+1 can be downloaded while N has not aired yet and
// if we continue, the currentTrackStream will be emptied, causing a crash in
// notifyAudioReachedPlayback when it will look for trackInfo. A busy loop is never
// ideal, but this low impact, infrequent and more simple than yet another semaphore
while (currentSongPlaying && !isAiring()) {
BELL_SLEEP_MS(100);
}
// always move back to LOADING (ensure proper seeking after last track has been loaded)
this->currentTrackStream.reset();
this->playbackMutex.unlock();
if (eof) {
this->eofCallback();
}
}
}
size_t TrackPlayer::_vorbisRead(void* ptr, size_t size, size_t nmemb) {
if (this->currentTrackStream == nullptr) {
return 0;
}
return this->currentTrackStream->readBytes((uint8_t*)ptr, nmemb * size);
}
size_t TrackPlayer::_vorbisClose() {
return 0;
}
int TrackPlayer::_vorbisSeek(int64_t offset, int whence) {
if (this->currentTrackStream == nullptr) {
return 0;
}
switch (whence) {
case 0:
this->currentTrackStream->seek(offset); // Spotify header offset
break;
case 1:
this->currentTrackStream->seek(this->currentTrackStream->getPosition() +
offset);
break;
case 2:
this->currentTrackStream->seek(this->currentTrackStream->getSize() +
offset);
break;
}
return 0;
}
long TrackPlayer::_vorbisTell() {
if (this->currentTrackStream == nullptr) {
return 0;
}
return this->currentTrackStream->getPosition();
}
CDNTrackStream::TrackInfo TrackPlayer::getCurrentTrackInfo() {
return this->currentTrackStream->trackInfo;
}
void TrackPlayer::setDataCallback(DataCallback callback) {
this->dataCallback = callback;
}

View File

@@ -0,0 +1,184 @@
#include "TrackProvider.h"
#include <memory>
#include "AccessKeyFetcher.h"
#include "CDNTrackStream.h"
#include "Logger.h"
#include "MercurySession.h"
#include "TrackReference.h"
#include "Utils.h"
#include "protobuf/metadata.pb.h"
using namespace cspot;
TrackProvider::TrackProvider(std::shared_ptr<cspot::Context> ctx) {
this->accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
this->ctx = ctx;
this->cdnStream =
std::make_unique<cspot::CDNTrackStream>(this->accessKeyFetcher);
this->trackInfo = {};
}
TrackProvider::~TrackProvider() {
pb_release(Track_fields, &trackInfo);
}
std::shared_ptr<cspot::CDNTrackStream> TrackProvider::loadFromTrackRef(TrackReference& trackRef) {
auto track = std::make_shared<cspot::CDNTrackStream>(this->accessKeyFetcher);
this->currentTrackReference = track;
this->trackIdInfo = trackRef;
queryMetadata();
return track;
}
void TrackProvider::queryMetadata() {
std::string requestUrl = string_format(
"hm://metadata/3/%s/%s", trackIdInfo.type == TrackReference::Type::TRACK ? "track" : "episode",
bytesToHexString(trackIdInfo.gid).c_str());
CSPOT_LOG(debug, "Requesting track metadata from %s", requestUrl.c_str());
auto responseHandler = [this](MercurySession::Response& res) {
this->onMetadataResponse(res);
};
// Execute the request
ctx->session->execute(MercurySession::RequestType::GET, requestUrl,
responseHandler);
}
void TrackProvider::onMetadataResponse(MercurySession::Response& res) {
CSPOT_LOG(debug, "Got track metadata response");
pb_release(Track_fields, &trackInfo);
pbDecode(trackInfo, Track_fields, res.parts[0]);
CSPOT_LOG(info, "Track name: %s", trackInfo.name);
CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
trackInfo.restriction_count);
int altIndex = -1;
while (!canPlayTrack(altIndex)) {
altIndex++;
CSPOT_LOG(info, "Trying alternative %d", altIndex);
if (altIndex >= trackInfo.alternative_count) {
// no alternatives for song
if (!this->currentTrackReference.expired()) {
auto trackRef = this->currentTrackReference.lock();
trackRef->status = CDNTrackStream::Status::FAILED;
trackRef->trackReady->give();
}
return;
}
}
std::vector<uint8_t> trackId;
std::vector<uint8_t> fileId;
AudioFormat format = AudioFormat_OGG_VORBIS_160;
if (altIndex < 0) {
trackId = pbArrayToVector(trackInfo.gid);
for (int x = 0; x < trackInfo.file_count; x++) {
if (trackInfo.file[x].format == format) {
fileId = pbArrayToVector(trackInfo.file[x].file_id);
break; // If file found stop searching
}
}
} else {
trackId = pbArrayToVector(trackInfo.alternative[altIndex].gid);
for (int x = 0; x < trackInfo.alternative[altIndex].file_count; x++) {
if (trackInfo.alternative[altIndex].file[x].format == format) {
fileId =
pbArrayToVector(trackInfo.alternative[altIndex].file[x].file_id);
break; // If file found stop searching
}
}
}
if (!this->currentTrackReference.expired()) {
auto trackRef = this->currentTrackReference.lock();
auto imageId =
pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
trackRef->trackInfo.name = std::string(trackInfo.name);
trackRef->trackInfo.album = std::string(trackInfo.album.name);
trackRef->trackInfo.artist = std::string(trackInfo.artist[0].name);
trackRef->trackInfo.imageUrl =
"https://i.scdn.co/image/" + bytesToHexString(imageId);
trackRef->trackInfo.duration = trackInfo.duration;
}
this->fetchFile(fileId, trackId);
}
void TrackProvider::fetchFile(const std::vector<uint8_t>& fileId,
const std::vector<uint8_t>& trackId) {
ctx->session->requestAudioKey(
trackId, fileId,
[this, fileId](bool success, const std::vector<uint8_t>& audioKey) {
if (success) {
CSPOT_LOG(info, "Got audio key");
if (!this->currentTrackReference.expired()) {
auto ref = this->currentTrackReference.lock();
ref->fetchFile(fileId, audioKey);
}
} else {
CSPOT_LOG(error, "Failed to get audio key");
if (!this->currentTrackReference.expired()) {
auto ref = this->currentTrackReference.lock();
ref->fail();
}
}
});
}
bool countryListContains(char* countryList, char* country) {
uint16_t countryList_length = strlen(countryList);
for (int x = 0; x < countryList_length; x += 2) {
if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
return true;
}
}
return false;
}
bool TrackProvider::canPlayTrack(int altIndex) {
if (altIndex < 0) {
for (int x = 0; x < trackInfo.restriction_count; x++) {
if (trackInfo.restriction[x].countries_allowed != nullptr) {
return countryListContains(trackInfo.restriction[x].countries_allowed,
(char*)ctx->config.countryCode.c_str());
}
if (trackInfo.restriction[x].countries_forbidden != nullptr) {
return !countryListContains(
trackInfo.restriction[x].countries_forbidden,
(char*)ctx->config.countryCode.c_str());
}
}
} else {
for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
x++) {
if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
nullptr) {
return countryListContains(
trackInfo.alternative[altIndex].restriction[x].countries_allowed,
(char*)ctx->config.countryCode.c_str());
}
if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
nullptr) {
return !countryListContains(
trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
(char*)ctx->config.countryCode.c_str());
}
}
}
return true;
}

View File

@@ -1,38 +0,0 @@
#include "TrackReference.h"
#include "Logger.h"
TrackReference::TrackReference(TrackRef *ref)
{
if (ref->gid != nullptr)
{
gid = pbArrayToVector(ref->gid);
}
else if (ref->uri != nullptr)
{
auto uri = std::string(ref->uri);
auto idString = uri.substr(uri.find_last_of(":") + 1, uri.size());
CSPOT_LOG(debug, "idString = %s", idString.c_str());
gid = base62Decode(idString);
isEpisode = true;
}
}
TrackReference::~TrackReference()
{
//pb_release(TrackRef_fields, &ref);
//pbFree(TrackRef_fields, &ref);
}
std::vector<uint8_t> TrackReference::base62Decode(std::string uri)
{
std::vector<uint8_t> n = std::vector<uint8_t>({0});
for (int x = 0; x < uri.size(); x++)
{
size_t d = alphabet.find(uri[x]);
n = bigNumMultiply(n, 62);
n = bigNumAdd(n, d);
}
return n;
}

View File

@@ -23,7 +23,20 @@ uint64_t hton64(uint64_t value) {
}
}
std::string bytesToHexString(std::vector<uint8_t>& v) {
std::vector<uint8_t> stringHexToBytes(const std::string & s) {
std::vector<uint8_t> v;
v.reserve(s.length() / 2);
for (std::string::size_type i = 0; i < s.length(); i += 2) {
std::string byteString = s.substr(i, 2);
uint8_t byte = (uint8_t) strtol(byteString.c_str(), NULL, 16);
v.push_back(byte);
}
return v;
}
std::string bytesToHexString(const std::vector<uint8_t>& v) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
std::vector<uint8_t>::const_iterator it;
@@ -64,6 +77,28 @@ std::vector<uint8_t> bigNumAdd(std::vector<uint8_t> num, int n)
return num;
}
std::vector<uint8_t> bigNumDivide(std::vector<uint8_t> num, int n)
{
auto carry = 0;
for (int x = 0; x < num.size(); x++)
{
int res = num[x] + carry * 256;
if (res < n)
{
carry = res;
num[x] = 0;
}
else
{
// Carry the rest of the division
carry = res % n;
num[x] = res / n;
}
}
return num;
}
std::vector<uint8_t> bigNumMultiply(std::vector<uint8_t> num, int n)
{
auto carry = 0;

View File

@@ -1,184 +0,0 @@
#include "ZeroconfAuthenticator.h"
#include "JSONObject.h"
#include <sstream>
#ifndef _WIN32
#include <sys/select.h>
#else
#include <iphlpapi.h>
#pragma comment(lib, "IPHLPAPI.lib")
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include "Logger.h"
#include "CspotAssert.h"
#include "ConfigJSON.h"
// provide weak deviceId (see ConstantParameters.h)
#if _MSC_VER
char deviceId[] = "142137fd329622137a14901634264e6f332e2411";
#else
char deviceId[] __attribute__((weak)) = "142137fd329622137a14901634264e6f332e2411";
#endif
ZeroconfAuthenticator::ZeroconfAuthenticator(authCallback callback, std::shared_ptr<bell::BaseHTTPServer> httpServer) {
this->gotBlobCallback = callback;
srand((unsigned int)time(NULL));
this->crypto = std::make_unique<Crypto>();
this->crypto->dhInit();
this->server = httpServer;
#ifdef _WIN32
char hostname[128];
gethostname(hostname, sizeof(hostname));
struct sockaddr_in* host = NULL;
ULONG size = sizeof(IP_ADAPTER_ADDRESSES) * 32;
IP_ADAPTER_ADDRESSES* adapters = (IP_ADAPTER_ADDRESSES*) malloc(size);
int ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST, 0, adapters, &size);
for (PIP_ADAPTER_ADDRESSES adapter = adapters; adapter && !host; adapter = adapter->Next) {
if (adapter->TunnelType == TUNNEL_TYPE_TEREDO) continue;
if (adapter->OperStatus != IfOperStatusUp) continue;
for (IP_ADAPTER_UNICAST_ADDRESS* unicast = adapter->FirstUnicastAddress; unicast;
unicast = unicast->Next) {
if (adapter->FirstGatewayAddress && unicast->Address.lpSockaddr->sa_family == AF_INET) {
host = (struct sockaddr_in*)unicast->Address.lpSockaddr;
BELL_LOG(info, "mdns", "mDNS on interface %s", inet_ntoa(host->sin_addr));
this->service = mdnsd_start(host->sin_addr, false);
break;
}
}
}
CSPOT_ASSERT(this->service, "can't start mDNS service");
mdnsd_set_hostname(this->service, hostname, host->sin_addr);
#endif
}
void ZeroconfAuthenticator::registerHandlers() {
// Make it discoverable for spoti clients
registerZeroconf();
auto getInfoHandler = [this](std::unique_ptr<bell::HTTPRequest> request) {
CSPOT_LOG(info, "Got request for info");
bell::HTTPResponse response = {
.connectionFd = request->connection,
.status = 200,
.body = this->buildJsonInfo(),
.contentType = "application/json",
};
server->respond(response);
};
auto addUserHandler = [this](std::unique_ptr<bell::HTTPRequest> request) {
BELL_LOG(info, "http", "Got request for adding user");
bell::JSONObject obj;
obj["status"] = 101;
obj["spotifyError"] = 0;
obj["statusString"] = "ERROR-OK";
bell::HTTPResponse response = {
.connectionFd = request->connection,
.status = 200,
.body = obj.toString(),
.contentType = "application/json",
};
server->respond(response);
auto correctBlob = this->getParameterFromUrlEncoded(request->body, "blob");
this->handleAddUser(request->queryParams);
};
BELL_LOG(info, "cspot", "Zeroconf registering handlers");
this->server->registerHandler(bell::RequestType::GET, "/spotify_info", getInfoHandler);
this->server->registerHandler(bell::RequestType::POST, "/spotify_info", addUserHandler);
}
void ZeroconfAuthenticator::registerZeroconf()
{
const char* service = "_spotify-connect._tcp";
#ifdef ESP_PLATFORM
mdns_txt_item_t serviceTxtData[3] = {
{"VERSION", "1.0"},
{"CPath", "/spotify_info"},
{"Stack", "SP"} };
mdns_service_add("cspot", "_spotify-connect", "_tcp", this->server->serverPort, serviceTxtData, 3);
#elif _WIN32
const char *serviceTxtData[] = {
"VERSION=1.0",
"CPath=/spotify_info",
"Stack=SP",
NULL };
mdnsd_register_svc(this->service, "cspot", "_spotify-connect._tcp.local", this->server->serverPort, NULL, serviceTxtData);
#else
DNSServiceRef ref = NULL;
TXTRecordRef txtRecord;
TXTRecordCreate(&txtRecord, 0, NULL);
TXTRecordSetValue(&txtRecord, "VERSION", 3, "1.0");
TXTRecordSetValue(&txtRecord, "CPath", 13, "/spotify_info");
TXTRecordSetValue(&txtRecord, "Stack", 2, "SP");
DNSServiceRegister(&ref, 0, 0, (char*)informationString, service, NULL, NULL, htons(this->server->serverPort), TXTRecordGetLength(&txtRecord), TXTRecordGetBytesPtr(&txtRecord), NULL, NULL);
TXTRecordDeallocate(&txtRecord);
#endif
}
std::string ZeroconfAuthenticator::getParameterFromUrlEncoded(std::string data, std::string param)
{
auto startStr = data.substr(data.find("&" + param + "=") + param.size() + 2, data.size());
return urlDecode(startStr.substr(0, startStr.find("&")));
}
void ZeroconfAuthenticator::handleAddUser(std::map<std::string, std::string>& queryData)
{
// Get all urlencoded params
auto username = queryData["userName"];
auto blobString = queryData["blob"];
auto clientKeyString = queryData["clientKey"];
auto deviceName = queryData["deviceName"];
// client key and bytes are urlencoded
auto clientKeyBytes = crypto->base64Decode(clientKeyString);
auto blobBytes = crypto->base64Decode(blobString);
// Generated secret based on earlier generated DH
auto secretKey = crypto->dhCalculateShared(clientKeyBytes);
auto loginBlob = std::make_shared<LoginBlob>();
std::string deviceIdStr = deviceId;
loginBlob->loadZeroconf(blobBytes, secretKey, deviceIdStr, username);
gotBlobCallback(loginBlob);
}
std::string ZeroconfAuthenticator::buildJsonInfo()
{
// Encode publicKey into base64
auto encodedKey = crypto->base64Encode(crypto->publicKey);
bell::JSONObject obj;
obj["status"] = 101;
obj["statusString"] = "OK";
obj["version"] = protocolVersion;
obj["spotifyError"] = 0;
obj["libraryVersion"] = swVersion;
obj["accountReq"] = "PREMIUM";
obj["brandDisplayName"] = brandName;
obj["modelDisplayName"] = configMan->deviceName.c_str();
obj["voiceSupport"] = "NO";
obj["availability"] = "";
obj["productID"] = 0;
obj["tokenType"] = "default";
obj["groupStatus"] = "NONE";
obj["resolverVersion"] = "0";
obj["scope"] = "streaming,client-authorization-universal";
obj["activeUser"] = "";
obj["deviceID"] = deviceId;
obj["remoteName"] = configMan->deviceName.c_str();
obj["publicKey"] = encodedKey;
obj["deviceType"] = "SPEAKER";
return obj.toString();
}