mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2025-12-09 21:17:18 +03:00
big merge
This commit is contained in:
97
components/spotify/cspot/src/ApResolve.cpp
Normal file
97
components/spotify/cspot/src/ApResolve.cpp
Normal 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;
|
||||
}
|
||||
41
components/spotify/cspot/src/AudioChunk.cpp
Normal file
41
components/spotify/cspot/src/AudioChunk.cpp
Normal 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);
|
||||
}
|
||||
121
components/spotify/cspot/src/AudioChunkManager.cpp
Normal file
121
components/spotify/cspot/src/AudioChunkManager.cpp
Normal 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
|
||||
}
|
||||
331
components/spotify/cspot/src/ChunkedAudioStream.cpp
Normal file
331
components/spotify/cspot/src/ChunkedAudioStream.cpp
Normal 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, ¤tSection);
|
||||
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;
|
||||
}
|
||||
94
components/spotify/cspot/src/ConfigJSON.cpp
Normal file
94
components/spotify/cspot/src/ConfigJSON.cpp
Normal 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());
|
||||
}
|
||||
133
components/spotify/cspot/src/LoginBlob.cpp
Normal file
133
components/spotify/cspot/src/LoginBlob.cpp
Normal 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();
|
||||
}
|
||||
356
components/spotify/cspot/src/MercuryManager.cpp
Normal file
356
components/spotify/cspot/src/MercuryManager.cpp
Normal 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);
|
||||
}
|
||||
33
components/spotify/cspot/src/MercuryResponse.cpp
Normal file
33
components/spotify/cspot/src/MercuryResponse.cpp
Normal 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);
|
||||
}
|
||||
6
components/spotify/cspot/src/Packet.cpp
Normal file
6
components/spotify/cspot/src/Packet.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "Packet.h"
|
||||
|
||||
Packet::Packet(uint8_t command, std::vector<uint8_t> &data) {
|
||||
this->command = command;
|
||||
this->data = data;
|
||||
};
|
||||
115
components/spotify/cspot/src/PbReader.cpp
Normal file
115
components/spotify/cspot/src/PbReader.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
142
components/spotify/cspot/src/PbWriter.cpp
Normal file
142
components/spotify/cspot/src/PbWriter.cpp
Normal 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);
|
||||
}
|
||||
170
components/spotify/cspot/src/PlainConnection.cpp
Normal file
170
components/spotify/cspot/src/PlainConnection.cpp
Normal 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);
|
||||
}
|
||||
131
components/spotify/cspot/src/Player.cpp
Normal file
131
components/spotify/cspot/src/Player.cpp
Normal 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);
|
||||
};
|
||||
}
|
||||
199
components/spotify/cspot/src/PlayerState.cpp
Normal file
199
components/spotify/cspot/src/PlayerState.cpp
Normal 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);
|
||||
}
|
||||
189
components/spotify/cspot/src/Protobuf.cpp
Normal file
189
components/spotify/cspot/src/Protobuf.cpp
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
155
components/spotify/cspot/src/Session.cpp
Normal file
155
components/spotify/cspot/src/Session.cpp
Normal 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);
|
||||
}
|
||||
443
components/spotify/cspot/src/Shannon.cpp
Normal file
443
components/spotify/cspot/src/Shannon.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
components/spotify/cspot/src/ShannonConnection.cpp
Normal file
100
components/spotify/cspot/src/ShannonConnection.cpp
Normal 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;
|
||||
}
|
||||
232
components/spotify/cspot/src/SpircController.cpp
Normal file
232
components/spotify/cspot/src/SpircController.cpp
Normal 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);
|
||||
}
|
||||
166
components/spotify/cspot/src/SpotifyTrack.cpp
Normal file
166
components/spotify/cspot/src/SpotifyTrack.cpp
Normal 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);
|
||||
}
|
||||
15
components/spotify/cspot/src/TimeProvider.cpp
Normal file
15
components/spotify/cspot/src/TimeProvider.cpp
Normal 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;
|
||||
}
|
||||
36
components/spotify/cspot/src/TrackReference.cpp
Normal file
36
components/spotify/cspot/src/TrackReference.cpp
Normal 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;
|
||||
}
|
||||
133
components/spotify/cspot/src/Utils.cpp
Normal file
133
components/spotify/cspot/src/Utils.cpp
Normal 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;
|
||||
}
|
||||
139
components/spotify/cspot/src/ZeroconfAuthenticator.cpp
Normal file
139
components/spotify/cspot/src/ZeroconfAuthenticator.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user