big merge

This commit is contained in:
Philippe G
2021-12-18 21:04:23 -08:00
parent 955692f8ad
commit 898998efb0
583 changed files with 84472 additions and 1965 deletions

View File

@@ -0,0 +1,97 @@
#include "ApResolve.h"
#include <memory>
#include <vector>
#include <string>
#include <iostream>
#include <ctype.h>
#include <cstring>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sstream>
#include <fstream>
#include "Logger.h"
#include <cJSON.h>
ApResolve::ApResolve() {}
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;
}
return jsonData;
}
std::string ApResolve::fetchFirstApAddress()
{
// Fetch json body
auto jsonData = getApList();
// 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;
}

View File

@@ -0,0 +1,41 @@
#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>(2);
this->isLoadedSemaphore = std::make_unique<WrappedSemaphore>(2);
}
AudioChunk::~AudioChunk()
{
}
void AudioChunk::appendData(std::vector<uint8_t> &data)
{
this->decryptedData.insert(this->decryptedData.end(), data.begin(), data.end());
}
void AudioChunk::decrypt()
{
// calculate the IV for right position
auto calculatedIV = this->getIVSum(startPosition / 16);
crypto->aesCTRXcrypt(this->audioKey, calculatedIV, decryptedData);
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

@@ -0,0 +1,121 @@
#include "AudioChunkManager.h"
#include "BellUtils.h"
#include "Logger.h"
AudioChunkManager::AudioChunkManager()
: bell::Task("AudioChunkManager", 4 * 1024, +0, 0) {
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) {
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() {
// 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() {
this->isRunning = true;
std::scoped_lock lock(this->runningMutex);
while (isRunning) {
std::pair<std::vector<uint8_t>, bool> audioPair;
if (this->audioChunkDataQueue.wtpop(audioPair, 100)) {
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(),
[](const 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 decrypt!", 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: Starting decrypt!",
seqId);
chunk->decrypt();
chunk->isLoadedSemaphore->give();
break;
default:
// printf("ID: %d: Got data chunk!\n", seqId);
// 2 first bytes are size so we skip it
// printf("(_)--- Free memory %d\n",
// esp_get_free_heap_size());
if (chunk == nullptr) {
return;
}
auto actualData = std::vector<uint8_t>(
data.begin() + 2, data.end());
chunk->appendData(actualData);
break;
}
}
}
} catch (...) {
}
} else {
}
}
// Playback finished
}

View File

@@ -0,0 +1,331 @@
#include "ChunkedAudioStream.h"
#include "Logger.h"
#include "BellUtils.h"
static size_t vorbisReadCb(void *ptr, size_t size, size_t nmemb, ChunkedAudioStream *self)
{
auto data = self->read(nmemb);
std::copy(data.begin(), data.end(), (char *)ptr);
return data.size();
}
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->pos);
}
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->audioKey = audioKey;
this->duration = duration;
this->manager = manager;
this->fileId = fileId;
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();
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)
{
this->seekMutex.lock();
loadingMeta = true;
ov_time_seek(&vorbisFile, positionMs);
loadingMeta = false;
this->seekMutex.unlock();
CSPOT_LOG(debug, "--- Finished seeking!");
}
void ChunkedAudioStream::startPlaybackLoop()
{
loadingMeta = true;
isRunning = true;
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);
}
else
{
this->requestChunk(0);
}
loadingMeta = false;
bool eof = false;
while (!eof && isRunning)
{
if (!isPaused)
{
std::vector<uint8_t> pcmOut(4096 / 4);
this->seekMutex.lock();
long ret = ov_read(&vorbisFile, (char *)&pcmOut[0], 4096 / 4, &currentSection);
this->seekMutex.unlock();
if (ret == 0)
{
// 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
auto data = std::vector<uint8_t>(pcmOut.begin(), pcmOut.begin() + ret);
pcmCallback(data);
// 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);
}
std::vector<uint8_t> ChunkedAudioStream::read(size_t bytes)
{
auto toRead = bytes;
auto res = std::vector<uint8_t>();
READ:
while (res.size() < bytes)
{
auto position = pos;
auto isLoadingMeta = loadingMeta;
// Erase all chunks not close to current position
chunks.erase(std::remove_if(
chunks.begin(), chunks.end(),
[position, &isLoadingMeta](const std::shared_ptr<AudioChunk> &chunk) {
if (isLoadingMeta) {
return false;
}
if (chunk->keepInMemory)
{
return false;
}
if (chunk->isFailed)
{
return true;
}
if (chunk->endPosition < position || chunk->startPosition > position + BUFFER_SIZE)
{
return true;
}
return false;
}),
chunks.end());
int16_t chunkIndex = this->pos / AUDIO_CHUNK_SIZE;
int32_t offset = this->pos % AUDIO_CHUNK_SIZE;
if (pos >= fileSize)
{
CSPOT_LOG(debug, "EOL!");
return res;
}
auto chunk = findChunkForPosition(pos);
if (chunk != nullptr)
{
auto offset = pos - chunk->startPosition;
if (chunk->isLoaded)
{
if (chunk->decryptedData.size() - offset >= toRead)
{
if((chunk->decryptedData.begin() + offset) < chunk->decryptedData.end()) {
res.insert(res.end(), chunk->decryptedData.begin() + offset,
chunk->decryptedData.begin() + offset + toRead);
this->pos += toRead;
} else {
chunk->decrypt();
}
}
else
{
res.insert(res.end(), chunk->decryptedData.begin() + offset, chunk->decryptedData.end());
this->pos += chunk->decryptedData.size() - offset;
toRead -= chunk->decryptedData.size() - offset;
}
}
else
{
CSPOT_LOG(debug, "Waiting for chunk to load");
while (chunk->isLoadedSemaphore->twait() != 0);
if (chunk->isFailed)
{
auto requestChunk = this->requestChunk(chunkIndex);
while (requestChunk->isLoadedSemaphore->twait() != 0);
goto READ;
}
}
}
else
{
CSPOT_LOG(debug, "Actual request %d", chunkIndex);
this->requestChunk(chunkIndex);
}
}
if (!loadingMeta)
{
auto requestedOffset = 0;
while (requestedOffset < BUFFER_SIZE)
{
auto chunk = findChunkForPosition(pos + requestedOffset);
if (chunk != nullptr)
{
requestedOffset = chunk->endPosition - pos;
// Don not buffer over EOL - unnecessary "failed chunks"
if ((pos + requestedOffset) >= fileSize)
{
break;
}
}
else
{
auto chunkReq = manager->fetchAudioChunk(fileId, audioKey, (pos + requestedOffset) / 4, (pos + requestedOffset + AUDIO_CHUNK_SIZE) / 4);
CSPOT_LOG(debug, "Chunk req end pos %d", chunkReq->endPosition);
this->chunks.push_back(chunkReq);
}
}
}
return res;
}
std::shared_ptr<AudioChunk> ChunkedAudioStream::findChunkForPosition(size_t position)
{
for (int i = 0; i < this->chunks.size(); i++)
{
auto chunk = this->chunks[i];
if (chunk->startPosition <= position && chunk->endPosition > position)
{
return chunk;
}
}
return nullptr;
}
void ChunkedAudioStream::seek(size_t dpos, Whence whence)
{
switch (whence)
{
case Whence::START:
this->pos = dpos;
break;
case Whence::CURRENT:
this->pos += dpos;
break;
case Whence::END:
this->pos = fileSize + dpos;
break;
}
auto currentChunk = this->pos / AUDIO_CHUNK_SIZE;
if (findChunkForPosition(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(manager->fetchAudioChunk(fileId, audioKey, startPosition, startPosition + (AUDIO_CHUNK_SIZE / 4)));
}
CSPOT_LOG(debug, "Change in current chunk %d", currentChunk);
}
std::shared_ptr<AudioChunk> ChunkedAudioStream::requestChunk(size_t chunkIndex)
{
CSPOT_LOG(debug, "Chunk Req %d", chunkIndex);
auto chunk = manager->fetchAudioChunk(fileId, audioKey, chunkIndex);
this->chunks.push_back(chunk);
return chunk;
}

View File

@@ -0,0 +1,94 @@
#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

@@ -0,0 +1,133 @@
#include "LoginBlob.h"
#include "JSONObject.h"
#include "Logger.h"
LoginBlob::LoginBlob()
{
this->crypto = std::make_unique<Crypto>();
}
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);
auto checksumMessage = std::string("checksum");
auto checksumKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(checksumMessage.begin(), checksumMessage.end()));
auto encryptionMessage = std::string("encryption");
auto encryptionKey = crypto->sha1HMAC(baseKey, std::vector<uint8_t>(encryptionMessage.begin(), encryptionMessage.end()));
auto mac = crypto->sha1HMAC(checksumKey, encrypted);
// Check checksum
if (mac != checksum)
{
CSPOT_LOG(error, "Mac doesn't match!" );
}
encryptionKey = std::vector<uint8_t>(encryptionKey.begin(), encryptionKey.begin() + 16);
crypto->aesCTRXcrypt(encryptionKey, iv, encrypted);
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;
}
auto hi = data[blobSkipPosition + 1];
this->blobSkipPosition += 2;
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);
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->aesECBdecrypt(key, blobData);
auto l = blobData.size();
for (int i = 0; i < l - 16; i++)
{
blobData[l - i - 1] ^= blobData[l - i - 17];
}
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);
// 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::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");
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);
}
std::string LoginBlob::toJson()
{
bell::JSONObject obj;
obj["authData"] = crypto->base64Encode(authData);
obj["authType"] = this->authType;
obj["username"] = this->username;
return obj.toString();
}

View File

@@ -0,0 +1,356 @@
#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", 4 * 1024, +1, 1)
{
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();
};
}
bool MercuryManager::timeoutHandler()
{
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
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(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)
{
// Reconnection required
this->reconnect();
this->reconnectedCallback();
continue;
}
if (static_cast<MercuryType>(packet->command) == MercuryType::PING) // @TODO: Handle time synchronization through ping
{
CSPOT_LOG(debug, "Got ping, syncing timestamp");
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() {
CSPOT_LOG(debug, "Stopping mercury manager");
isRunning = false;
audioChunkManager->close();
std::scoped_lock(audioChunkManager->runningMutex, this->runningMutex);
this->session->close();
CSPOT_LOG(debug, "mercury stopped");
}
void MercuryManager::updateQueue() {
if (queueSemaphore->twait() == 0) {
if (this->queue.size() > 0)
{
auto packet = std::move(this->queue[0]);
this->queue.erase(this->queue.begin());
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:
{
countryCode = std::string(packet->data.begin(), packet->data.end());
CSPOT_LOG(debug, "Received country code: %s", countryCode.c_str());
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);
if (this->subscriptions.count(response->mercuryHeader.uri.value()) > 0)
{
this->subscriptions[response->mercuryHeader.uri.value()](std::move(response));
//this->subscriptions.erase(std::string(response->mercuryHeader.uri));
}
break;
}
default:
break;
}
}
}
}
void MercuryManager::handleQueue()
{
while (isRunning)
{
this->updateQueue();
}
}
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());
Header mercuryHeader;
mercuryHeader.uri = uri;
mercuryHeader.method = MercuryTypeMap[method];
// 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 = encodePb(mercuryHeader);
// 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

@@ -0,0 +1,33 @@
#include "MercuryResponse.h"
MercuryResponse::MercuryResponse(std::vector<uint8_t> &data)
{
// this->mercuryHeader = std::make_unique<Header>();
this->parts = mercuryParts(0);
this->parseResponse(data);
}
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;
}
this->mercuryHeader = decodePb<Header>(headerBytes);
}

View File

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

View File

@@ -0,0 +1,115 @@
#include "PbReader.h"
#include <iostream>
PbReader::PbReader(std::vector<uint8_t> const &rawData) : rawData(rawData)
{
maxPosition = rawData.size();
}
template <typename T>
T PbReader::decodeVarInt()
{
uint8_t byte;
uint_fast8_t bitpos = 0;
uint64_t storago = 0;
do
{
byte = this->rawData[pos];
pos++;
storago |= (uint64_t)(byte & 0x7F) << bitpos;
bitpos = (uint_fast8_t)(bitpos + 7);
} while (byte & 0x80);
return static_cast<T>(storago);
}
template <typename T>
T PbReader::decodeFixed()
{
pos += sizeof(T);
return *(T*)(&this->rawData[pos - sizeof(T)]);
}
template int32_t PbReader::decodeFixed();
template int64_t PbReader::decodeFixed();
template uint32_t PbReader::decodeVarInt();
template int64_t PbReader::decodeVarInt();
template bool PbReader::decodeVarInt();
void PbReader::resetMaxPosition()
{
maxPosition = rawData.size();
}
void PbReader::decodeString(std::string &target)
{
nextFieldLength = decodeVarInt<uint32_t>();
target.resize(nextFieldLength);
// std::cout << "rawData.size() = " << rawData.size() << " pos = " << pos << " nextFieldLength =" << nextFieldLength;
// printf("\n%d, \n", currentTag);
// if (pos + nextFieldLength >= rawData.size())
// {
// std::cout << " \nBAD -- pos + nextFieldLength >= rawData.size() MSVC IS LITERLALLY SHAKING AND CRYING RN";
// }
// std::cout << std::endl;
std::copy(rawData.begin() + pos, rawData.begin() + pos + nextFieldLength, target.begin());
pos += nextFieldLength;
}
void PbReader::decodeVector(std::vector<uint8_t> &target)
{
nextFieldLength = decodeVarInt<uint32_t>();
target.resize(nextFieldLength);
std::copy(rawData.begin() + pos, rawData.begin() + pos + nextFieldLength, target.begin());
pos += nextFieldLength;
}
bool PbReader::next()
{
if (pos >= maxPosition)
return false;
currentWireValue = decodeVarInt<uint32_t>();
currentTag = currentWireValue >> 3U;
currentWireType = PbWireType(currentWireValue & 0x07U);
return true;
}
int64_t PbReader::decodeZigzag(uint64_t value)
{
return static_cast<int64_t>((value >> 1U) ^ static_cast<uint64_t>(-static_cast<int64_t>(value & 1U)));
}
template <typename T>
T PbReader::decodeSVarInt()
{
skipVarIntDump = decodeVarInt<uint64_t>();
return static_cast<T>(decodeZigzag(skipVarIntDump));
}
template int32_t PbReader::decodeSVarInt();
template int64_t PbReader::decodeSVarInt();
void PbReader::skip()
{
switch (currentWireType)
{
case PbWireType::varint:
skipVarIntDump = decodeVarInt<uint64_t>();
break;
case PbWireType::fixed64:
pos += 8;
break;
case PbWireType::length_delimited:
nextFieldLength = decodeVarInt<uint32_t>();
pos += nextFieldLength;
break;
case PbWireType::fixed32:
pos += 4;
break;
default:
break;
}
}

View File

@@ -0,0 +1,142 @@
#include "PbWriter.h"
PbWriter::PbWriter(std::vector<uint8_t> &rawData) : rawData(rawData)
{
}
void PbWriter::encodeVarInt(uint32_t low, uint32_t high, int32_t indexOffset)
{
size_t i = 0;
uint8_t byte = (uint8_t)(low & 0x7F);
low >>= 7;
while (i < 4 && (low != 0 || high != 0))
{
byte |= 0x80;
rawData.insert(rawData.end() + indexOffset, byte);
i++;
byte = (uint8_t)(low & 0x7F);
low >>= 7;
}
if (high)
{
byte = (uint8_t)(byte | ((high & 0x07) << 4));
high >>= 3;
while (high)
{
byte |= 0x80;
rawData.insert(rawData.end() + indexOffset, byte);
i++;
byte = (uint8_t)(high & 0x7F);
high >>= 7;
}
}
rawData.insert(rawData.end() + indexOffset, byte);
}
template void PbWriter::encodeVarInt(uint8_t, int32_t);
template void PbWriter::encodeVarInt(uint32_t, int32_t);
template void PbWriter::encodeVarInt(uint64_t, int32_t);
template void PbWriter::encodeVarInt(long long, int32_t);
template <typename T>
void PbWriter::encodeVarInt(T data, int32_t offset)
{
auto value = static_cast<uint64_t>(data);
if (value <= 0x7F)
{
rawData.insert(rawData.end() + offset, (uint8_t)value);
}
else
{
encodeVarInt((uint32_t)value, (uint32_t)(value >> 32), offset);
}
}
uint32_t PbWriter::encodeZigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1U) ^ static_cast<uint32_t>(-static_cast<int32_t>(static_cast<uint32_t>(value) >> 31U));
}
uint64_t PbWriter::encodeZigzag64(int64_t value) {
return (static_cast<uint64_t>(value) << 1U) ^ static_cast<uint64_t>(-static_cast<int64_t>(static_cast<uint64_t>(value) >> 63U));
}
void PbWriter::addSVarInt32(uint32_t tag, int32_t data) {
auto val = encodeZigzag32(data);
addVarInt(tag, val);
}
template <typename T>
void PbWriter::encodeFixed(T data) {
auto val = reinterpret_cast<const char*>(&data);
rawData.insert(rawData.end(), val, val + sizeof(T));
}
template void PbWriter::encodeFixed(int64_t);
template void PbWriter::encodeFixed(int32_t);
void PbWriter::addSVarInt64(uint32_t tag, int64_t data) {
auto val = encodeZigzag64(data);
addVarInt(tag, val);
}
void PbWriter::addString(uint32_t tag, std::string &target)
{
addField(tag, PbWireType::length_delimited);
uint32_t stringSize = target.size();
encodeVarInt(stringSize);
rawData.insert(rawData.end(), target.begin(), target.end());
}
void PbWriter::addVector(uint32_t tag, std::vector<uint8_t> &target)
{
addField(tag, PbWireType::length_delimited);
uint32_t vectorSize = target.size();
encodeVarInt(vectorSize);
rawData.insert(rawData.end(), target.begin(), target.end());
}
template <typename T>
void PbWriter::addVarInt(uint32_t tag, T intType)
{
addField(tag, PbWireType::varint);
encodeVarInt(intType);
}
void PbWriter::addBool(uint32_t tag, bool value)
{
addField(tag, PbWireType::varint);
rawData.push_back(char(value));
}
template void PbWriter::addVarInt(uint32_t, uint8_t);
template void PbWriter::addVarInt(uint32_t, uint32_t);
template void PbWriter::addVarInt(uint32_t, uint64_t);
template void PbWriter::addVarInt(uint32_t, int64_t);
template void PbWriter::addVarInt(uint32_t, bool);
void PbWriter::addField(uint32_t tag, PbWireType wiretype)
{
const uint32_t value = (tag << 3U) | uint32_t(wiretype);
encodeVarInt(value);
}
uint32_t PbWriter::startMessage()
{
return rawData.size();
}
void PbWriter::finishMessage(uint32_t tag, uint32_t lastMessagePosition)
{
uint32_t finalMessageSize = rawData.size() - lastMessagePosition;
uint32_t msgHeader = (tag << 3U) | uint32_t(PbWireType::length_delimited);
int32_t offset = -finalMessageSize;
encodeVarInt(msgHeader, offset);
encodeVarInt(finalMessageSize, offset);
}

View File

@@ -0,0 +1,170 @@
#include "PlainConnection.h"
#include <cstring>
#include <netinet/tcp.h>
#include <errno.h>
#include "Logger.h"
PlainConnection::PlainConnection(){};
PlainConnection::~PlainConnection()
{
closeSocket();
};
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;
// 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;
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)
{
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
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");
}
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());
return sizeData;
}
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());
// Actually write it to the server
writeBlock(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;
// printf("START READ\n");
while (idx < size)
{
READ:
if ((n = recv(this->apSock, &buf[idx], size - idx, 0)) <= 0)
{
switch (errno)
{
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:
throw std::runtime_error("Corn");
}
}
idx += n;
}
// printf("FINISH READ\n");
return buf;
}
size_t PlainConnection::writeBlock(const std::vector<uint8_t> &data)
{
unsigned int idx = 0;
ssize_t n;
// printf("START WRITE\n");
while (idx < data.size())
{
WRITE:
if ((n = send(this->apSock, &data[idx], data.size() - idx < 64 ? data.size() - idx : 64, 0)) <= 0)
{
switch (errno)
{
case EAGAIN:
case ETIMEDOUT:
if (timeoutHandler())
{
throw std::runtime_error("Reconnection required");
}
goto WRITE;
case EINTR:
break;
default:
throw std::runtime_error("Corn");
}
}
idx += n;
}
return data.size();
}
void PlainConnection::closeSocket()
{
CSPOT_LOG(info, "Closing socket...");
shutdown(this->apSock, SHUT_RDWR);
close(this->apSock);
}

View File

@@ -0,0 +1,131 @@
#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, +0, 1)
{
this->audioSink = audioSink;
this->manager = manager;
startTask();
}
void Player::pause()
{
this->currentTrack->audioStream->isPaused = true;
}
void Player::play()
{
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)
{
this->currentTrack->audioStream->seekMs(positionMs);
// VALGRIND_DO_LEAK_CHECK;
}
void Player::feedPCM(std::vector<uint8_t>& data)
{
// Simple digital volume control alg
// @TODO actually extract it somewhere
if (this->audioSink->softwareVolumeControl)
{
int16_t* psample;
uint32_t pmax;
psample = (int16_t*)(data.data());
for (int32_t i = 0; i < (data.size() / 2); i++)
{
int32_t temp;
// 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);
}
void Player::runTask()
{
std::scoped_lock lock(this->runningMutex);
this->isRunning = true;
while (isRunning)
{
if (this->trackQueue.wpop(currentTrack)) {
currentTrack->audioStream->startPlaybackLoop();
currentTrack->loadedTrackCallback = nullptr;
currentTrack->audioStream->streamFinishedCallback = nullptr;
currentTrack->audioStream->audioSink = nullptr;
currentTrack->audioStream->pcmCallback = nullptr;
}
}
}
void Player::stop() {
this->isRunning = false;
CSPOT_LOG(info, "Stopping player");
this->trackQueue.clear();
cancelCurrentTrack();
CSPOT_LOG(info, "Track cancelled");
std::scoped_lock lock(this->runningMutex);
CSPOT_LOG(info, "Done");
}
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()>& trackLoadedCallback, uint32_t position_ms, bool isPaused)
{
std::lock_guard<std::mutex> guard(loadTrackMutex);
cancelCurrentTrack();
pcmDataCallback framesCallback = [=](std::vector<uint8_t>& frames) {
this->feedPCM(frames);
};
auto loadedLambda = trackLoadedCallback;
auto track = std::make_shared<SpotifyTrack>(this->manager, trackReference, position_ms, isPaused);
track->trackInfoReceived = this->trackChanged;
track->loadedTrackCallback = [this, track, framesCallback, loadedLambda]() {
loadedLambda();
track->audioStream->streamFinishedCallback = this->endOfFileCallback;
track->audioStream->audioSink = this->audioSink;
track->audioStream->pcmCallback = framesCallback;
this->trackQueue.push(track);
};
}

View File

@@ -0,0 +1,199 @@
#include "PlayerState.h"
#include "Logger.h"
#include "ConfigJSON.h"
PlayerState::PlayerState(std::shared_ptr<TimeProvider> timeProvider)
{
this->timeProvider = timeProvider;
// Prepare default state
innerFrame.state.emplace();
innerFrame.state->position_ms = 0;
innerFrame.state->status = PlayStatus::kPlayStatusStop;
innerFrame.state->position_measured_at = 0;
innerFrame.state->shuffle = false;
innerFrame.state->repeat = false;
innerFrame.device_state.emplace();
innerFrame.device_state->sw_version = swVersion;
innerFrame.device_state->is_active = false;
innerFrame.device_state->can_play = true;
innerFrame.device_state->volume = configMan->volume;
innerFrame.device_state->name = configMan->deviceName;
// Prepare player's capabilities
innerFrame.device_state->capabilities = std::vector<Capability>();
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"}));
}
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.value();
this->updatePositionMs(innerFrame.state->position_ms.value() + diff);
break;
}
}
bool PlayerState::isActive()
{
return innerFrame.device_state->is_active.value();
}
bool PlayerState::nextTrack()
{
innerFrame.state->playing_track_index.value()++;
if (innerFrame.state->playing_track_index >= innerFrame.state->track.size())
{
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.value()--;
}
else if (innerFrame.state->repeat)
{
innerFrame.state->playing_track_index = innerFrame.state->track.size() - 1;
}
}
void PlayerState::setActive(bool isActive)
{
innerFrame.device_state->is_active = isActive;
if (isActive)
{
innerFrame.device_state->became_active_at = timeProvider->getSyncedTimestamp();
}
}
void PlayerState::updatePositionMs(uint32_t position)
{
innerFrame.state->position_ms = position;
innerFrame.state->position_measured_at = timeProvider->getSyncedTimestamp();
}
void PlayerState::updateTracks()
{
CSPOT_LOG(info, "---- Track count %d", remoteFrame.state->track.size());
// innerFrame.state->context_uri = remoteFrame.state->context_uri == nullptr ? nullptr : strdup(otherFrame->state->context_uri);
innerFrame.state->track = remoteFrame.state->track;
innerFrame.state->playing_track_index = remoteFrame.state->playing_track_index;
if (remoteFrame.state->repeat.value())
{
setRepeat(true);
}
if (remoteFrame.state->shuffle.value())
{
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
auto tmp = innerFrame.state->track.at(0);
innerFrame.state->track.at(0) = innerFrame.state->track.at(innerFrame.state->playing_track_index.value());
innerFrame.state->track.at(innerFrame.state->playing_track_index.value()) = tmp;
// Shuffle current tracks
for (int x = 1; x < innerFrame.state->track.size() - 1; x++)
{
auto j = x + (std::rand() % (innerFrame.state->track.size() - x));
tmp = innerFrame.state->track.at(j);
innerFrame.state->track.at(j) = innerFrame.state->track.at(x);
innerFrame.state->track.at(x) = tmp;
}
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.at(innerFrame.state->playing_track_index.value()));
}
std::vector<uint8_t> PlayerState::encodeCurrentFrame(MessageType typ)
{
// Prepare current frame info
innerFrame.version = 1;
innerFrame.ident = deviceId;
innerFrame.seq_nr = this->seqNum;
innerFrame.protocol_version = protocolVersion;
innerFrame.typ = typ;
innerFrame.state_update_id = timeProvider->getSyncedTimestamp();
this->seqNum += 1;
auto fram = encodePb(innerFrame);
return fram;
}
// Wraps messy nanopb setters. @TODO: find a better way to handle this
void PlayerState::addCapability(CapabilityType typ, int intValue, std::vector<std::string> stringValue)
{
auto capability = Capability();
capability.typ = typ;
if (intValue != -1)
{
capability.intValue = std::vector<int64_t>({intValue});
}
capability.stringValue = stringValue;
innerFrame.device_state->capabilities.push_back(capability);
}

View File

@@ -0,0 +1,189 @@
#include "ProtoHelper.h"
std::optional<AnyRef> findFieldWithProtobufTag(AnyRef ref, uint32_t tag)
{
auto info = ref.reflectType();
for (int i = 0; i < info->fields.size(); i++)
{
if (tag == info->fields[i].protobufTag)
{
return std::make_optional(ref.getField(i));
}
}
return std::nullopt;
}
void decodeField(std::shared_ptr<PbReader> reader, AnyRef any)
{
auto fieldInfo = any.reflectType();
if (fieldInfo->kind == ReflectTypeKind::Optional)
{
auto optionalRef = AnyOptionalRef(any);
optionalRef.emplaceEmpty();
return decodeField(reader, optionalRef.get());
}
if (fieldInfo->kind == ReflectTypeKind::Class)
{
// Handle submessage
auto lastMaxPosition = reader->maxPosition;
auto messageSize = reader->decodeVarInt<uint32_t>();
reader->maxPosition = messageSize + reader->pos;
while (reader->next())
{
auto res = findFieldWithProtobufTag(any, reader->currentTag);
if (res.has_value())
{
decodeField(reader, res.value());
}
else
{
reader->skip();
}
}
reader->maxPosition = lastMaxPosition;
return;
} else if (any.is<std::vector<uint8_t>>())
{
reader->decodeVector(*any.as<std::vector<uint8_t>>());
}
// Handle repeated
else if (fieldInfo->kind == ReflectTypeKind::Vector)
{
auto aVec = AnyVectorRef(any);
aVec.emplace_back();
auto value = aVec.at(aVec.size() - 1);
auto valInfo = value.reflectType();
// Represents packed repeated encoding
if (valInfo->kind == ReflectTypeKind::Primitive && !value.is<std::string>() && !value.is<std::vector<uint8_t>>())
{
// *any.as<int64_t>() = reader->decodeVarInt<int64_t>();
reader->skip();
}
else
{
decodeField(reader, value);
}
}
else if (fieldInfo->kind == ReflectTypeKind::Enum)
{
*any.as<uint32_t>() = reader->decodeVarInt<uint32_t>();
}
else if (any.is<std::string>())
{
reader->decodeString(*any.as<std::string>());
}
else if (any.is<bool>())
{
*any.as<bool>() = reader->decodeVarInt<bool>();
}
else if (any.is<uint32_t>())
{
*any.as<uint32_t>() = reader->decodeVarInt<uint32_t>();
}
else if (any.is<int64_t>())
{
*any.as<int64_t>() = reader->decodeVarInt<int64_t>();
}
else
{
reader->skip();
}
}
void decodeProtobuf(std::shared_ptr<PbReader> reader, AnyRef any)
{
while (reader->next())
{
auto res = findFieldWithProtobufTag(any, reader->currentTag);
if (res.has_value())
{
decodeField(reader, res.value());
}
else
{
reader->skip();
}
}
}
void encodeProtobuf(std::shared_ptr<PbWriter> writer, AnyRef any, uint32_t protobufTag)
{
auto info = any.reflectType();
// Handle optionals, only encode if have value
if (info->kind == ReflectTypeKind::Optional)
{
auto optionalRef = AnyOptionalRef(any);
if (!optionalRef.has_value())
{
return;
}
else
{
return encodeProtobuf(writer, optionalRef.get(), protobufTag);
}
}
if (info->kind == ReflectTypeKind::Class)
{
uint32_t startMsgPosition;
// 0 - default value, indicating top level message
if (protobufTag > 0)
{
startMsgPosition = writer->startMessage();
}
for (int i = 0; i < info->fields.size(); i++)
{
auto field = any.getField(i);
encodeProtobuf(writer, field, info->fields[i].protobufTag);
}
if (protobufTag > 0)
{
writer->finishMessage(protobufTag, startMsgPosition);
}
} else if (any.is<std::vector<uint8_t>>())
{
writer->addVector(protobufTag, *any.as<std::vector<uint8_t>>());
}
else if (info->kind == ReflectTypeKind::Vector) {
auto aVec = AnyVectorRef(any);
auto size = aVec.size();
for (size_t i = 0; i < size; i++)
{
auto valueAt = aVec.at(i);
encodeProtobuf(writer, valueAt, protobufTag);
}
}
else if (info->kind == ReflectTypeKind::Enum) {
writer->addVarInt<uint32_t>(protobufTag, *any.as<uint32_t>());
}
else if (info->kind == ReflectTypeKind::Primitive)
{
if (any.is<std::string>())
{
writer->addString(protobufTag, *any.as<std::string>());
}
else if (any.is<uint32_t>())
{
writer->addVarInt<uint32_t>(protobufTag, *any.as<uint32_t>());
}
else if (any.is<uint64_t>())
{
writer->addVarInt<uint64_t>(protobufTag, *any.as<uint64_t>());
}
else if (any.is<int64_t>()) {
writer->addVarInt<int64_t>(protobufTag, *any.as<int64_t>());
} else if (any.is<bool>())
{
writer->addVarInt<bool>(protobufTag, *any.as<bool>());
}
}
}

View File

@@ -0,0 +1,155 @@
#include "Session.h"
#include "MercuryManager.h"
#include "Logger.h"
using random_bytes_engine = std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
Session::Session()
{
// Generates the public and priv key
this->crypto = std::make_unique<Crypto>();
this->shanConn = std::make_shared<ShannonConnection>();
}
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>();
this->conn = std::make_unique<PlainConnection>();
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);
}
std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob)
{
// save auth blob for reconnection purposes
authBlob = blob;
// prepare authentication request proto
authRequest.login_credentials.username = blob->username;
authRequest.login_credentials.auth_data = blob->authData;
authRequest.login_credentials.typ = static_cast<AuthenticationType>(blob->authType);
authRequest.system_info.cpu_family = CpuFamily::CPU_UNKNOWN;
authRequest.system_info.os = Os::OS_UNKNOWN;
authRequest.system_info.system_information_string = std::string(informationString);
authRequest.system_info.device_id = std::string(deviceId);
authRequest.version_string = std::string(versionString);
auto data = encodePb(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;
}
case AUTH_DECLINED_COMMAND:
{
CSPOT_LOG(error, "Authorization declined");
break;
}
default:
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());
apResponse = decodePb<APResponseMessage>(skipSize);
auto kkEy = apResponse.challenge->login_crypto_challenge.diffie_hellman->gs;
// Compute the diffie hellman shared key based on the response
auto sharedKey = this->crypto->dhCalculateShared(kkEy);
// 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!
clientResPlaintext.login_crypto_response = {};
clientResPlaintext.login_crypto_response.diffie_hellman.emplace();
clientResPlaintext.login_crypto_response.diffie_hellman->hmac = crypto->sha1HMAC(lastVec, data);
auto resultPacket = encodePb(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);
}
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
clientHello.login_crypto_hello.diffie_hellman.emplace();
clientHello.feature_set.emplace();
clientHello.login_crypto_hello.diffie_hellman->gc = this->crypto->publicKey;
clientHello.login_crypto_hello.diffie_hellman->server_keys_known = 1;
clientHello.build_info.product = Product::PRODUCT_PARTNER;
clientHello.build_info.platform = Platform::PLATFORM_LINUX_X86;
clientHello.build_info.version = 112800721;
clientHello.feature_set->autoupdate2 = true;
clientHello.cryptosuites_supported = std::vector<Cryptosuite>({Cryptosuite::CRYPTO_SUITE_SHANNON});
clientHello.padding = std::vector<uint8_t>({0x1E});
// Generate the random nonce
clientHello.client_nonce = crypto->generateVectorWithRandomData(16);
auto vecData = encodePb(clientHello);
auto prefix = std::vector<uint8_t>({0x00, 0x04});
return this->conn->sendPrefixPacket(prefix, vecData);
}

View File

@@ -0,0 +1,443 @@
#include "Shannon.h"
// #include <bit>
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
// #define NDEBUG
#include <assert.h>
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 &= 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);
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n >> c) | (n << ((-c) & mask));
}
uint32_t Shannon::sbox1(uint32_t w)
{
w ^= rotl(w, 5) | rotl(w, 7);
w ^= rotl(w, 19) | rotl(w, 22);
return w;
}
uint32_t Shannon::sbox2(uint32_t w)
{
w ^= rotl(w, 7) | rotl(w, 22);
w ^= rotl(w, 5) | rotl(w, 19);
return w;
}
void Shannon::cycle()
{
uint32_t t;
int i;
/* nonlinear feedback function */
t = this->R[12] ^ this->R[13] ^ this->konst;
t = Shannon::sbox1(t) ^ rotl(this->R[0], 1);
/* shift register */
for (i = 1; i < N; ++i)
this->R[i - 1] = this->R[i];
this->R[N - 1] = t;
t = Shannon::sbox2(this->R[2] ^ this->R[15]);
this->R[0] ^= t;
this->sbuf = t ^ this->R[8] ^ this->R[12];
}
void Shannon::crcfunc(uint32_t i)
{
uint32_t t;
int j;
/* Accumulate CRC of input */
t = this->CRC[0] ^ this->CRC[2] ^ this->CRC[15] ^ i;
for (j = 1; j < N; ++j)
this->CRC[j - 1] = this->CRC[j];
this->CRC[N - 1] = t;
}
void Shannon::macfunc(uint32_t i)
{
this->crcfunc(i);
this->R[KEYP] ^= i;
}
void Shannon::initState()
{
int i;
/* Register initialised to Fibonacci numbers; Counter zeroed. */
this->R[0] = 1;
this->R[1] = 1;
for (i = 2; i < N; ++i)
this->R[i] = this->R[i - 1] + this->R[i - 2];
this->konst = Shannon::INITKONST;
}
void Shannon::saveState()
{
int i;
for (i = 0; i < Shannon::N; ++i)
this->initR[i] = this->R[i];
}
void Shannon::reloadState()
{
int i;
for (i = 0; i < Shannon::N; ++i)
this->R[i] = this->initR[i];
}
void Shannon::genkonst()
{
this->konst = this->R[0];
}
void Shannon::diffuse()
{
int i;
for (i = 0; i < Shannon::FOLD; ++i)
this->cycle();
}
#define Byte(x, i) ((uint32_t)(((x) >> (8 * (i))) & 0xFF))
#define BYTE2WORD(b) ( \
(((uint32_t)(b)[3] & 0xFF) << 24) | \
(((uint32_t)(b)[2] & 0xFF) << 16) | \
(((uint32_t)(b)[1] & 0xFF) << 8) | \
(((uint32_t)(b)[0] & 0xFF)))
#define WORD2BYTE(w, b) \
{ \
(b)[3] = Byte(w, 3); \
(b)[2] = Byte(w, 2); \
(b)[1] = Byte(w, 1); \
(b)[0] = Byte(w, 0); \
}
#define XORWORD(w, b) \
{ \
(b)[3] ^= Byte(w, 3); \
(b)[2] ^= Byte(w, 2); \
(b)[1] ^= Byte(w, 1); \
(b)[0] ^= Byte(w, 0); \
}
#define XORWORD(w, b) \
{ \
(b)[3] ^= Byte(w, 3); \
(b)[2] ^= Byte(w, 2); \
(b)[1] ^= Byte(w, 1); \
(b)[0] ^= Byte(w, 0); \
}
/* Load key material into the register
*/
#define ADDKEY(k) \
this->R[KEYP] ^= (k);
void Shannon::loadKey(const std::vector<uint8_t> &key)
{
int i, j;
uint32_t k;
uint8_t xtra[4];
size_t keylen = key.size();
/* start folding in key */
for (i = 0; i < (keylen & ~0x3); i += 4)
{
k = BYTE2WORD(&key[i]);
ADDKEY(k);
this->cycle();
}
/* if there were any extra key bytes, zero pad to a word */
if (i < keylen)
{
for (j = 0 /* i unchanged */; i < keylen; ++i)
xtra[j++] = key[i];
for (/* j unchanged */; j < 4; ++j)
xtra[j] = 0;
k = BYTE2WORD(xtra);
ADDKEY(k);
this->cycle();
}
/* also fold in the length of the key */
ADDKEY(keylen);
this->cycle();
/* save a copy of the register */
for (i = 0; i < N; ++i)
this->CRC[i] = this->R[i];
/* now diffuse */
this->diffuse();
/* now xor the copy back -- makes key loading irreversible */
for (i = 0; i < N; ++i)
this->R[i] ^= this->CRC[i];
}
void Shannon::key(const std::vector<uint8_t> &key)
{
this->initState();
this->loadKey(key);
this->genkonst(); /* in case we proceed to stream generation */
this->saveState();
this->nbuf = 0;
}
void Shannon::nonce(const std::vector<uint8_t> &nonce)
{
this->reloadState();
this->konst = Shannon::INITKONST;
this->loadKey(nonce);
this->genkonst();
this->nbuf = 0;
}
void Shannon::stream(std::vector<uint8_t> &bufVec)
{
uint8_t *endbuf;
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
/* handle any previously buffered bytes */
while (this->nbuf != 0 && nbytes != 0)
{
*buf++ ^= this->sbuf & 0xFF;
this->sbuf >>= 8;
this->nbuf -= 8;
--nbytes;
}
/* handle whole words */
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
while (buf < endbuf)
{
this->cycle();
XORWORD(this->sbuf, buf);
buf += 4;
}
/* handle any trailing bytes */
nbytes &= 0x03;
if (nbytes != 0)
{
this->cycle();
this->nbuf = 32;
while (this->nbuf != 0 && nbytes != 0)
{
*buf++ ^= this->sbuf & 0xFF;
this->sbuf >>= 8;
this->nbuf -= 8;
--nbytes;
}
}
}
void Shannon::maconly(std::vector<uint8_t> &bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t *endbuf;
/* handle any previously buffered bytes */
if (this->nbuf != 0)
{
while (this->nbuf != 0 && nbytes != 0)
{
this->mbuf ^= (*buf++) << (32 - this->nbuf);
this->nbuf -= 8;
--nbytes;
}
if (this->nbuf != 0) /* not a whole word yet */
return;
/* LFSR already cycled */
this->macfunc(this->mbuf);
}
/* handle whole words */
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
while (buf < endbuf)
{
this->cycle();
this->macfunc(BYTE2WORD(buf));
buf += 4;
}
/* handle any trailing bytes */
nbytes &= 0x03;
if (nbytes != 0)
{
this->cycle();
this->mbuf = 0;
this->nbuf = 32;
while (this->nbuf != 0 && nbytes != 0)
{
this->mbuf ^= (*buf++) << (32 - this->nbuf);
this->nbuf -= 8;
--nbytes;
}
}
}
void Shannon::encrypt(std::vector<uint8_t> &bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t *endbuf;
uint32_t t = 0;
/* handle any previously buffered bytes */
if (this->nbuf != 0)
{
while (this->nbuf != 0 && nbytes != 0)
{
this->mbuf ^= *buf << (32 - this->nbuf);
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
++buf;
this->nbuf -= 8;
--nbytes;
}
if (this->nbuf != 0) /* not a whole word yet */
return;
/* LFSR already cycled */
this->macfunc(this->mbuf);
}
/* handle whole words */
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
while (buf < endbuf)
{
this->cycle();
t = BYTE2WORD(buf);
this->macfunc(t);
t ^= this->sbuf;
WORD2BYTE(t, buf);
buf += 4;
}
/* handle any trailing bytes */
nbytes &= 0x03;
if (nbytes != 0)
{
this->cycle();
this->mbuf = 0;
this->nbuf = 32;
while (this->nbuf != 0 && nbytes != 0)
{
this->mbuf ^= *buf << (32 - this->nbuf);
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
++buf;
this->nbuf -= 8;
--nbytes;
}
}
}
void Shannon::decrypt(std::vector<uint8_t> &bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
uint8_t *endbuf;
uint32_t t = 0;
/* handle any previously buffered bytes */
if (this->nbuf != 0)
{
while (this->nbuf != 0 && nbytes != 0)
{
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
this->mbuf ^= *buf << (32 - this->nbuf);
++buf;
this->nbuf -= 8;
--nbytes;
}
if (this->nbuf != 0) /* not a whole word yet */
return;
/* LFSR already cycled */
this->macfunc(this->mbuf);
}
/* handle whole words */
endbuf = &buf[nbytes & ~((uint32_t)0x03)];
while (buf < endbuf)
{
this->cycle();
t = BYTE2WORD(buf) ^ this->sbuf;
this->macfunc(t);
WORD2BYTE(t, buf);
buf += 4;
}
/* handle any trailing bytes */
nbytes &= 0x03;
if (nbytes != 0)
{
this->cycle();
this->mbuf = 0;
this->nbuf = 32;
while (this->nbuf != 0 && nbytes != 0)
{
*buf ^= (this->sbuf >> (32 - this->nbuf)) & 0xFF;
this->mbuf ^= *buf << (32 - this->nbuf);
++buf;
this->nbuf -= 8;
--nbytes;
}
}
}
void Shannon::finish(std::vector<uint8_t> &bufVec)
{
size_t nbytes = bufVec.size();
uint8_t *buf = bufVec.data();
int i;
/* handle any previously buffered bytes */
if (this->nbuf != 0)
{
/* LFSR already cycled */
this->macfunc(this->mbuf);
}
/* 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.
*/
this->cycle();
ADDKEY(INITKONST ^ (this->nbuf << 3));
this->nbuf = 0;
/* now add the CRC to the stream register and diffuse it */
for (i = 0; i < N; ++i)
this->R[i] ^= this->CRC[i];
this->diffuse();
/* produce output from the stream buffer */
while (nbytes > 0)
{
this->cycle();
if (nbytes >= 4)
{
WORD2BYTE(this->sbuf, buf);
nbytes -= 4;
buf += 4;
}
else
{
for (i = 0; i < nbytes; ++i)
buf[i] = Byte(this->sbuf, i);
break;
}
}
}

View File

@@ -0,0 +1,100 @@
#include "ShannonConnection.h"
#include "Logger.h"
ShannonConnection::ShannonConnection()
{
}
ShannonConnection::~ShannonConnection()
{
}
void ShannonConnection::wrapConnection(std::shared_ptr<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)));
}
void ShannonConnection::sendPacket(uint8_t cmd, std::vector<uint8_t> &data)
{
this->writeMutex.lock();
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);
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;
}

View File

@@ -0,0 +1,232 @@
#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();
}
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);
notify();
} else {
CSPOT_LOG(debug, "External play command");
if (notifyPlayer) player->play();
state->setPlaybackState(PlaybackState::Playing);
notify();
}
}
void SpircController::playToggle() {
if (state->innerFrame.state->status.value() == PlayStatus::kPlayStatusPause) {
setPause(false);
} else {
setPause(true);
}
}
void SpircController::adjustVolume(int by) {
if (state->innerFrame.device_state->volume.has_value()) {
int volume = state->innerFrame.device_state->volume.value() + 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) {
state->remoteFrame = decodePb<Frame>(data);
switch (state->remoteFrame.typ.value()) {
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.value()) {
sendEvent(CSpotEventType::DISC);
state->setActive(false);
notify();
player->cancelCurrentTrack();
}
break;
}
case MessageType::kMessageTypeSeek: {
CSPOT_LOG(debug, "Seek command");
sendEvent(CSpotEventType::SEEK, (int) state->remoteFrame.position.value());
state->updatePositionMs(state->remoteFrame.position.value());
this->player->seekMs(state->remoteFrame.position.value());
notify();
break;
}
case MessageType::kMessageTypeVolume:
sendEvent(CSpotEventType::VOLUME, (int) state->remoteFrame.volume.value());
setVolume(state->remoteFrame.volume.value());
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.value(), false);
state->updatePositionMs(state->remoteFrame.state->position_ms.value());
this->notify();
break;
}
case MessageType::kMessageTypeReplace: {
CSPOT_LOG(debug, "Got replace frame!");
break;
}
case MessageType::kMessageTypeShuffle: {
CSPOT_LOG(debug, "Got shuffle frame");
state->setShuffle(state->remoteFrame.state->shuffle.value());
this->notify();
break;
}
case MessageType::kMessageTypeRepeat: {
CSPOT_LOG(debug, "Got repeat frame");
state->setRepeat(state->remoteFrame.state->repeat.value());
this->notify();
break;
}
default:
break;
}
}
void SpircController::loadTrack(uint32_t position_ms, bool isPaused) {
state->setPlaybackState(PlaybackState::Loading);
std::function<void()> loadedLambda = [=]() {
// Loading finished, notify that playback started
setPause(isPaused, false);
};
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;
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,166 @@
#include "SpotifyTrack.h"
#include "unistd.h"
#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>();
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();
}
bool SpotifyTrack::countryListContains(std::string countryList, std::string country)
{
for (int x = 0; x < countryList.size(); x += 2)
{
if (countryList.substr(x, 2) == country)
{
return true;
}
}
return false;
}
bool SpotifyTrack::canPlayTrack(std::vector<Restriction>& restrictions)
{
for (int x = 0; x < restrictions.size(); x++)
{
if (restrictions[x].countries_allowed.has_value())
{
return countryListContains(restrictions[x].countries_allowed.value(), manager->countryCode);
}
if (restrictions[x].countries_forbidden.has_value())
{
return !countryListContains(restrictions[x].countries_forbidden.value(), 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");
trackInfo = decodePb<Track>(response->parts[0]);
CSPOT_LOG(info, "Track name: %s", trackInfo.name.value().c_str());
CSPOT_LOG(debug, "trackInfo.restriction.size() = %d", trackInfo.restriction.size());
int altIndex = 0;
while (!canPlayTrack(trackInfo.restriction))
{
trackInfo.restriction = trackInfo.alternative[altIndex].restriction;
trackInfo.gid = trackInfo.alternative[altIndex].gid;
trackInfo.file = trackInfo.alternative[altIndex].file;
altIndex++;
CSPOT_LOG(info, "Trying alternative %d", altIndex);
}
auto trackId = trackInfo.gid.value();
this->fileId = std::vector<uint8_t>();
for (int x = 0; x < trackInfo.file.size(); x++)
{
if (trackInfo.file[x].format == configMan->format)
{
this->fileId = trackInfo.file[x].file_id.value();
break; // If file found stop searching
}
}
if (trackInfoReceived != nullptr)
{
CSPOT_LOG(info, "Calling %d", trackInfo.album.value().cover_group.value().image.size());
TrackInfo simpleTrackInfo = {
.name = trackInfo.name.value(),
.album = trackInfo.album.value().name.value(),
.artist = trackInfo.artist[0].name.value(),
.imageUrl = "https://i.scdn.co/image/" + bytesToHexString(trackInfo.album.value().cover_group.value().image[0].file_id.value())
};
trackInfoReceived(simpleTrackInfo);
}
this->requestAudioKey(this->fileId, trackId, trackInfo.duration.value(), 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");
episodeInfo = decodePb<Episode>(response->parts[0]);
CSPOT_LOG(info, "--- Episode name: %s", episodeInfo.name.value().c_str());
this->fileId = std::vector<uint8_t>();
// TODO: option to set file quality
for (int x = 0; x < episodeInfo.audio.size(); x++)
{
if (episodeInfo.audio[x].format == AudioFormat::OGG_VORBIS_96)
{
this->fileId = episodeInfo.audio[x].file_id.value();
break; // If file found stop searching
}
}
this->requestAudioKey(episodeInfo.gid.value(), this->fileId, episodeInfo.duration.value(), 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

@@ -0,0 +1,15 @@
#include "TimeProvider.h"
#include "Utils.h"
TimeProvider::TimeProvider() {
}
void TimeProvider::syncWithPingPacket(const std::vector<uint8_t>& pongPacket) {
// 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();
}
unsigned long long TimeProvider::getSyncedTimestamp() {
return getCurrentTimestamp() + this->timestampDiff;
}

View File

@@ -0,0 +1,36 @@
#include "TrackReference.h"
#include "Logger.h"
TrackReference::TrackReference(TrackRef *ref)
{
if (ref->gid.has_value())
{
gid = ref->gid.value();
}
else if (ref->uri.has_value())
{
auto uri = ref->uri.value();
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()
{
}
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

@@ -0,0 +1,133 @@
#include "Utils.h"
#include <cstring>
#include <memory>
#include <chrono>
#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>
unsigned long long getCurrentTimestamp()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
uint64_t hton64(uint64_t value) {
int num = 42;
if (*(char *)&num == 42) {
uint32_t high_part = htonl((uint32_t)(value >> 32));
uint32_t low_part = htonl((uint32_t)(value & 0xFFFFFFFFLL));
return (((uint64_t)low_part) << 32) | high_part;
} else {
return value;
}
}
std::string bytesToHexString(std::vector<uint8_t>& v) {
std::stringstream ss;
ss << std::hex << std::setfill('0');
std::vector<uint8_t>::const_iterator it;
for (it = v.begin(); it != v.end(); it++) {
ss << std::setw(2) << static_cast<unsigned>(*it);
}
return ss.str();
}
std::vector<uint8_t> bigNumAdd(std::vector<uint8_t> num, int n)
{
auto carry = n;
for (int x = num.size() - 1; x >= 0; x--)
{
int res = num[x] + carry;
if (res < 256)
{
carry = 0;
num[x] = res;
}
else
{
// Carry the rest of the division
carry = res / 256;
num[x] = res % 256;
// extend the vector at the last index
if (x == 0)
{
num.insert(num.begin(), carry);
return num;
}
}
}
return num;
}
std::vector<uint8_t> bigNumMultiply(std::vector<uint8_t> num, int n)
{
auto carry = 0;
for (int x = num.size() - 1; x >= 0; x--)
{
int res = num[x] * n + carry;
if (res < 256)
{
carry = 0;
num[x] = res;
}
else
{
// Carry the rest of the division
carry = res / 256;
num[x] = res % 256;
// extend the vector at the last index
if (x == 0)
{
num.insert(num.begin(), carry);
return num;
}
}
}
return num;
}
unsigned char h2int(char c)
{
if (c >= '0' && c <='9'){
return((unsigned char)c - '0');
}
if (c >= 'a' && c <='f'){
return((unsigned char)c - 'a' + 10);
}
if (c >= 'A' && c <='F'){
return((unsigned char)c - 'A' + 10);
}
return(0);
}
std::string urlDecode(std::string str)
{
std::string encodedString="";
char c;
char code0;
char code1;
for (int i =0; i < str.length(); i++){
c=str[i];
if (c == '+'){
encodedString+=' ';
}else if (c == '%') {
i++;
code0=str[i];
i++;
code1=str[i];
c = (h2int(code0) << 4) | h2int(code1);
encodedString+=c;
} else{
encodedString+=c;
}
}
return encodedString;
}

View File

@@ -0,0 +1,139 @@
#include "ZeroconfAuthenticator.h"
#include "JSONObject.h"
#include <sstream>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "Logger.h"
#include "ConfigJSON.h"
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;
}
void ZeroconfAuthenticator::registerHandlers() {
// Make it discoverable for spoti clients
registerZeroconf();
auto getInfoHandler = [this](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](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_init();
mdns_hostname_set("cspot");
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);
#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();
}