Added support for authentication in Calibre

This commit is contained in:
Katie Paxton-Fear 2026-01-13 12:16:03 +00:00
parent 79dc134b78
commit da3abbacd5
6 changed files with 159 additions and 5 deletions

View File

@ -12,9 +12,9 @@
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 2;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 17; constexpr uint8_t SETTINGS_COUNT = 19;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -47,6 +47,8 @@ bool CrossPointSettings::saveToFile() const {
serialization::writeString(outputFile, std::string(opdsServerUrl)); serialization::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing); serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writeString(outputFile, std::string(calibreUsername));
serialization::writeString(outputFile, std::string(calibrePassword));
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -113,6 +115,20 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hideBatteryPercentage); serialization::readPod(inputFile, hideBatteryPercentage);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
{
std::string usernameStr;
serialization::readString(inputFile, usernameStr);
strncpy(calibreUsername, usernameStr.c_str(), sizeof(calibreUsername) - 1);
calibreUsername[sizeof(calibreUsername) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
{
std::string passwordStr;
serialization::readString(inputFile, passwordStr);
strncpy(calibrePassword, passwordStr.c_str(), sizeof(calibrePassword) - 1);
calibrePassword[sizeof(calibrePassword) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -88,6 +88,8 @@ class CrossPointSettings {
uint8_t screenMargin = 5; uint8_t screenMargin = 5;
// OPDS browser settings // OPDS browser settings
char opdsServerUrl[128] = ""; char opdsServerUrl[128] = "";
char calibreUsername[64] = "";
char calibrePassword[64] = "";
// Hide battery percentage // Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;

View File

@ -41,6 +41,10 @@ inline std::vector<SettingInfo> getSettingsList() {
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::String("opdsServerUrl", "Calibre Web URL", SETTINGS.opdsServerUrl, SettingInfo::String("opdsServerUrl", "Calibre Web URL", SETTINGS.opdsServerUrl,
sizeof(SETTINGS.opdsServerUrl) - 1), sizeof(SETTINGS.opdsServerUrl) - 1),
SettingInfo::String("calibreUsername", "Calibre Username", SETTINGS.calibreUsername,
sizeof(SETTINGS.calibreUsername) - 1),
SettingInfo::String("calibrePassword", "Calibre Password", SETTINGS.calibrePassword,
sizeof(SETTINGS.calibrePassword) - 1),
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("Check for updates"), SettingInfo::Action("Check for updates"),
}; };

View File

@ -13,8 +13,8 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEMS = 2; constexpr int MENU_ITEMS = 4;
const char* menuNames[MENU_ITEMS] = {"Calibre Web URL", "Connect as Wireless Device"}; const char* menuNames[MENU_ITEMS] = {"Calibre Web URL", "Username", "Password", "Connect as Wireless Device"};
} // namespace } // namespace
void CalibreSettingsActivity::taskTrampoline(void* param) { void CalibreSettingsActivity::taskTrampoline(void* param) {
@ -98,6 +98,42 @@ void CalibreSettingsActivity::handleSelection() {
updateRequired = true; updateRequired = true;
})); }));
} else if (selectedIndex == 1) { } else if (selectedIndex == 1) {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Username", SETTINGS.calibreUsername, 10,
63, // maxLength
false, // not password
[this](const std::string& username) {
strncpy(SETTINGS.calibreUsername, username.c_str(), sizeof(SETTINGS.calibreUsername) - 1);
SETTINGS.calibreUsername[sizeof(SETTINGS.calibreUsername) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
updateRequired = true;
},
[this]() {
exitActivity();
updateRequired = true;
}));
} else if (selectedIndex == 2) {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Password", SETTINGS.calibrePassword, 10,
63, // maxLength
true, // is password
[this](const std::string& password) {
strncpy(SETTINGS.calibrePassword, password.c_str(), sizeof(SETTINGS.calibrePassword) - 1);
SETTINGS.calibrePassword[sizeof(SETTINGS.calibrePassword) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
updateRequired = true;
},
[this]() {
exitActivity();
updateRequired = true;
}));
} else if (selectedIndex == 3) {
// Wireless Device - launch the activity (handles WiFi connection internally) // Wireless Device - launch the activity (handles WiFi connection internally)
exitActivity(); exitActivity();
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
@ -153,11 +189,19 @@ void CalibreSettingsActivity::render() {
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected);
// Draw status for URL setting // Draw status for URL, username, and password settings
if (i == 0) { if (i == 0) {
const char* status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; const char* status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]";
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
} else if (i == 1) {
const char* status = (strlen(SETTINGS.calibreUsername) > 0) ? "[Set]" : "[Not Set]";
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
} else if (i == 2) {
const char* status = (strlen(SETTINGS.calibrePassword) > 0) ? "[Set]" : "[Not Set]";
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
} }
} }

View File

@ -1,5 +1,8 @@
#include "UrlUtils.h" #include "UrlUtils.h"
#include <sstream>
#include <iomanip>
namespace UrlUtils { namespace UrlUtils {
std::string ensureProtocol(const std::string& url) { std::string ensureProtocol(const std::string& url) {
@ -38,4 +41,76 @@ std::string buildUrl(const std::string& serverUrl, const std::string& path) {
return urlWithProtocol + "/" + path; return urlWithProtocol + "/" + path;
} }
std::string urlEncode(const std::string& value) {
std::ostringstream escaped;
escaped.fill('0');
escaped << std::hex;
for (char c : value) {
// Keep alphanumeric and other safe characters intact
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
escaped << c;
} else {
// Encode special characters
escaped << std::uppercase;
escaped << '%' << std::setw(2) << int((unsigned char)c);
escaped << std::nouppercase;
}
}
return escaped.str();
}
std::string buildUrlWithAuth(const std::string& serverUrl, const std::string& path,
const std::string& username, const std::string& password) {
// If no credentials, use regular buildUrl
if (username.empty() && password.empty()) {
return buildUrl(serverUrl, path);
}
std::string urlWithProtocol = ensureProtocol(serverUrl);
// Find protocol end
const size_t protocolEnd = urlWithProtocol.find("://");
if (protocolEnd == std::string::npos) {
return buildUrl(serverUrl, path); // Fallback if no protocol
}
// Extract protocol and host parts
std::string protocol = urlWithProtocol.substr(0, protocolEnd + 3); // Include ://
std::string hostAndPath = urlWithProtocol.substr(protocolEnd + 3);
// Check if auth already exists in URL
const size_t atPos = hostAndPath.find('@');
if (atPos != std::string::npos) {
// Auth already in URL, remove it
hostAndPath = hostAndPath.substr(atPos + 1);
}
// Build auth string with URL encoding
std::string auth;
if (!username.empty() || !password.empty()) {
auth = urlEncode(username) + ":" + urlEncode(password) + "@";
}
// Reconstruct URL with auth
std::string authenticatedUrl = protocol + auth + hostAndPath;
// Now apply path logic
if (path.empty()) {
return authenticatedUrl;
}
if (path[0] == '/') {
// Absolute path - extract just protocol + auth + host
const size_t firstSlash = hostAndPath.find('/');
std::string hostOnly = (firstSlash == std::string::npos) ? hostAndPath : hostAndPath.substr(0, firstSlash);
return protocol + auth + hostOnly + path;
}
// Relative path
if (authenticatedUrl.back() == '/') {
return authenticatedUrl + path;
}
return authenticatedUrl + "/" + path;
}
} // namespace UrlUtils } // namespace UrlUtils

View File

@ -20,4 +20,17 @@ std::string extractHost(const std::string& url);
*/ */
std::string buildUrl(const std::string& serverUrl, const std::string& path); std::string buildUrl(const std::string& serverUrl, const std::string& path);
/**
* URL encode a string (percent encoding for special characters)
*/
std::string urlEncode(const std::string& value);
/**
* Build URL with basic authentication embedded.
* If username and password are provided, adds them to the URL.
* Example: https://username:password@example.com/path
*/
std::string buildUrlWithAuth(const std::string& serverUrl, const std::string& path,
const std::string& username, const std::string& password);
} // namespace UrlUtils } // namespace UrlUtils