feat: Add network credential settings for FTP and HTTP servers, add editable credential changes for hotspot

This commit is contained in:
altsysrq 2026-01-04 16:56:43 -06:00
parent 1aaec3d98d
commit 90e20b49b7
10 changed files with 334 additions and 24 deletions

View File

@ -47,6 +47,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [ ] Sleep wallpaper picker - [ ] Sleep wallpaper picker
- [x] Adjustable sleep timer - [x] Adjustable sleep timer
- [x] Set default folder - [x] Set default folder
- [x] Custom credentials for Hotspot and FTP/HTTP access
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.

View File

@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 22; constexpr uint8_t SETTINGS_COUNT = 28;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -49,6 +49,12 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, scheduleNetworkMode); serialization::writePod(outputFile, scheduleNetworkMode);
serialization::writePod(outputFile, scheduleHour); serialization::writePod(outputFile, scheduleHour);
serialization::writePod(outputFile, scheduleAutoShutdown); serialization::writePod(outputFile, scheduleAutoShutdown);
serialization::writeString(outputFile, ftpUsername);
serialization::writeString(outputFile, ftpPassword);
serialization::writeString(outputFile, httpUsername);
serialization::writeString(outputFile, httpPassword);
serialization::writeString(outputFile, apSsid);
serialization::writeString(outputFile, apPassword);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -119,6 +125,18 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, scheduleAutoShutdown); serialization::readPod(inputFile, scheduleAutoShutdown);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, ftpUsername);
if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, ftpPassword);
if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, httpUsername);
if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, httpPassword);
if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, apSsid);
if (++settingsRead >= fileSettingsCount) break;
serialization::readString(inputFile, apPassword);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -88,6 +88,14 @@ class CrossPointSettings {
// Custom default folder path (used when defaultFolder == FOLDER_CUSTOM) // Custom default folder path (used when defaultFolder == FOLDER_CUSTOM)
std::string customDefaultFolder = "/books"; std::string customDefaultFolder = "/books";
// Network credentials
std::string ftpUsername = "crosspoint";
std::string ftpPassword = "reader";
std::string httpUsername = "crosspoint";
std::string httpPassword = "reader";
std::string apSsid = "CrossPoint-Reader";
std::string apPassword = ""; // Empty = open network
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance

View File

@ -18,8 +18,6 @@
namespace { namespace {
// AP Mode configuration // AP Mode configuration
constexpr const char* AP_SSID = "CrossPoint-Reader";
constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
constexpr const char* AP_HOSTNAME = "crosspoint"; constexpr const char* AP_HOSTNAME = "crosspoint";
constexpr uint8_t AP_CHANNEL = 1; constexpr uint8_t AP_CHANNEL = 1;
constexpr uint8_t AP_MAX_CONNECTIONS = 4; constexpr uint8_t AP_MAX_CONNECTIONS = 4;
@ -219,11 +217,11 @@ void FileTransferActivity::startAccessPoint() {
// Start soft AP // Start soft AP
bool apStarted; bool apStarted;
if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) { if (!SETTINGS.apPassword.empty() && SETTINGS.apPassword.length() >= 8) {
apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD, AP_CHANNEL, false, AP_MAX_CONNECTIONS); apStarted = WiFi.softAP(SETTINGS.apSsid.c_str(), SETTINGS.apPassword.c_str(), AP_CHANNEL, false, AP_MAX_CONNECTIONS);
} else { } else {
// Open network (no password) // Open network (no password or password too short)
apStarted = WiFi.softAP(AP_SSID, nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS); apStarted = WiFi.softAP(SETTINGS.apSsid.c_str(), nullptr, AP_CHANNEL, false, AP_MAX_CONNECTIONS);
} }
if (!apStarted) { if (!apStarted) {
@ -239,10 +237,10 @@ void FileTransferActivity::startAccessPoint() {
char ipStr[16]; char ipStr[16];
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]);
connectedIP = ipStr; connectedIP = ipStr;
connectedSSID = AP_SSID; connectedSSID = SETTINGS.apSsid;
Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis()); Serial.printf("[%lu] [WEBACT] Access Point started!\n", millis());
Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), AP_SSID); Serial.printf("[%lu] [WEBACT] SSID: %s\n", millis(), SETTINGS.apSsid.c_str());
Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str()); Serial.printf("[%lu] [WEBACT] IP: %s\n", millis(), connectedIP.c_str());
// Start mDNS for hostname resolution // Start mDNS for hostname resolution
@ -492,6 +490,13 @@ void FileTransferActivity::renderHttpServerRunning() const {
// Show QR code for URL // Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:");
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
// Show HTTP credentials at the bottom
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 14, "Browser will ask for credentials:");
std::string httpUserStr = "Username: " + SETTINGS.httpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 15, httpUserStr.c_str());
std::string httpPassStr = "Password: " + SETTINGS.httpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 16, httpPassStr.c_str());
} else { } else {
// STA mode display (original behavior) // STA mode display (original behavior)
const int startY = 65; const int startY = 65;
@ -518,6 +523,13 @@ void FileTransferActivity::renderHttpServerRunning() const {
// Show QR code for URL // Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:");
// Show HTTP credentials
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 12, "Browser will ask for credentials:");
std::string httpUserStr = "Username: " + SETTINGS.httpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 13, httpUserStr.c_str());
std::string httpPassStr = "Password: " + SETTINGS.httpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 14, httpPassStr.c_str());
} }
const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); const auto labels = mappedInput.mapLabels("« Exit", "", "", "");
@ -542,7 +554,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3,
"or scan QR code with your phone to connect to WiFi."); "or scan QR code with your phone to connect to WiFi.");
// Show QR code for WiFi // Show QR code for WiFi
std::string wifiConfig = std::string("WIFI:T:WPA;S:") + connectedSSID + ";P:" + "" + ";;"; std::string wifiConfig = std::string("WIFI:T:") +
(SETTINGS.apPassword.empty() ? "" : "WPA") +
";S:" + connectedSSID +
";P:" + SETTINGS.apPassword + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
startY += 6 * 29 + 3 * LINE_SPACING; startY += 6 * 29 + 3 * LINE_SPACING;
@ -554,8 +569,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 4, ftpInfo.c_str(), true, BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 4, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Connect with FTP client:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Connect with FTP client:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Username: crosspoint"); std::string ftpUserStr = "Username: " + SETTINGS.ftpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, "Password: reader"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, ftpUserStr.c_str());
std::string ftpPassStr = "Password: " + SETTINGS.ftpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 7, ftpPassStr.c_str());
} else { } else {
// STA mode display // STA mode display
const int startY = 65; const int startY = 65;
@ -576,8 +593,10 @@ void FileTransferActivity::renderFtpServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, ftpInfo.c_str(), true, BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, ftpInfo.c_str(), true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Use FTP client to connect:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Use FTP client to connect:");
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Username: crosspoint"); std::string ftpUserStr = "Username: " + SETTINGS.ftpUsername;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "Password: reader"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, ftpUserStr.c_str());
std::string ftpPassStr = "Password: " + SETTINGS.ftpPassword;
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, ftpPassStr.c_str());
// Show QR code for FTP URL // Show QR code for FTP URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 8, ftpInfo); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 8, ftpInfo);

View File

@ -0,0 +1,198 @@
#include "CredentialSettingsActivity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "fontIds.h"
namespace {
constexpr int FIELD_COUNT = 6;
const char* FIELD_NAMES[FIELD_COUNT] = {
"FTP Username",
"FTP Password",
"HTTP Username",
"HTTP Password",
"Hotspot SSID",
"Hotspot Password"
};
} // namespace
void CredentialSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<CredentialSettingsActivity*>(param);
self->displayTaskLoop();
}
void CredentialSettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0;
updateRequired = true;
xTaskCreate(&CredentialSettingsActivity::taskTrampoline, "CredentialSettingsTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void CredentialSettingsActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void CredentialSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
selectCurrentField();
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex > 0) ? (selectedIndex - 1) : (FIELD_COUNT - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % FIELD_COUNT;
updateRequired = true;
}
}
void CredentialSettingsActivity::selectCurrentField() {
std::string* targetField = nullptr;
const char* promptText = "";
switch (selectedIndex) {
case 0: // FTP Username
targetField = &SETTINGS.ftpUsername;
promptText = "Enter FTP username:";
break;
case 1: // FTP Password
targetField = &SETTINGS.ftpPassword;
promptText = "Enter FTP password:";
break;
case 2: // HTTP Username
targetField = &SETTINGS.httpUsername;
promptText = "Enter HTTP username:";
break;
case 3: // HTTP Password
targetField = &SETTINGS.httpPassword;
promptText = "Enter HTTP password:";
break;
case 4: // Hotspot SSID
targetField = &SETTINGS.apSsid;
promptText = "Enter hotspot SSID:";
break;
case 5: // Hotspot Password
targetField = &SETTINGS.apPassword;
promptText = "Enter hotspot password (leave empty for open network):";
break;
}
if (targetField) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
bool isPassword = (selectedIndex == 1 || selectedIndex == 3 || selectedIndex == 5); // Password fields
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput,
promptText, // title
*targetField, // initialText
10, // startY
0, // maxLength (0 = unlimited)
isPassword, // isPassword
[this, targetField](const std::string& newValue) {
*targetField = newValue;
SETTINGS.saveToFile();
exitActivity();
updateRequired = true;
},
[this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
}
void CredentialSettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CredentialSettingsActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Network Credentials", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, 40, "Configure server and hotspot credentials");
// Draw selection
renderer.fillRect(0, 70 + selectedIndex * 35 - 2, pageWidth - 1, 35);
// Draw fields
for (int i = 0; i < FIELD_COUNT; i++) {
const int fieldY = 70 + i * 35;
const bool isSelected = (i == selectedIndex);
// Draw field name
renderer.drawText(UI_10_FONT_ID, 20, fieldY, FIELD_NAMES[i], !isSelected);
// Draw current value (masked for passwords)
std::string displayValue;
switch (i) {
case 0: // FTP Username
displayValue = SETTINGS.ftpUsername;
break;
case 1: // FTP Password
displayValue = SETTINGS.ftpPassword.empty() ? "" : std::string(SETTINGS.ftpPassword.length(), '*');
break;
case 2: // Hotspot SSID
displayValue = SETTINGS.apSsid;
break;
case 3: // Hotspot Password
displayValue = SETTINGS.apPassword.empty() ? "(open)" : std::string(SETTINGS.apPassword.length(), '*');
break;
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, displayValue.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, fieldY, displayValue.c_str(), !isSelected);
}
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Edit", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "activities/ActivityWithSubactivity.h"
/**
* CredentialSettingsActivity allows users to configure credentials for:
* - FTP server (username and password)
* - HTTP server (username and password)
* - WiFi Hotspot (SSID and password)
*/
class CredentialSettingsActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedIndex = 0; // Currently selected credential field
const std::function<void()> onGoBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void selectCurrentField();
public:
explicit CredentialSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CredentialSettings", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -3,6 +3,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CredentialSettingsActivity.h"
#include "FolderPickerActivity.h" #include "FolderPickerActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
@ -11,7 +12,7 @@
// Define the static settings list // Define the static settings list
namespace { namespace {
constexpr int settingsCount = 18; constexpr int settingsCount = 19;
const SettingInfo settingsList[settingsCount] = { const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
@ -51,6 +52,7 @@ const SettingInfo settingsList[settingsCount] = {
{"Root", "Custom", "Last Used"}}, {"Root", "Custom", "Last Used"}},
{"Choose Custom Folder", SettingType::ACTION, nullptr, {}}, {"Choose Custom Folder", SettingType::ACTION, nullptr, {}},
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}}, {"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
{"Network Credentials", SettingType::ACTION, nullptr, {}},
{"File Transfer Schedule", SettingType::ACTION, nullptr, {}}, {"File Transfer Schedule", SettingType::ACTION, nullptr, {}},
{"Check for updates", SettingType::ACTION, nullptr, {}}, {"Check for updates", SettingType::ACTION, nullptr, {}},
}; };
@ -169,6 +171,14 @@ void SettingsActivity::toggleCurrentSetting() {
}, },
"/")); // Start from root directory "/")); // Start from root directory
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} else if (std::string(setting.name) == "Network Credentials") {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CredentialSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (std::string(setting.name) == "File Transfer Schedule") { } else if (std::string(setting.name) == "File Transfer Schedule") {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();

View File

@ -2,11 +2,7 @@
#include <WiFi.h> #include <WiFi.h>
namespace { #include "CrossPointSettings.h"
// FTP server credentials
constexpr const char* FTP_USERNAME = "crosspoint";
constexpr const char* FTP_PASSWORD = "reader";
} // namespace
CrossPointFtpServer::CrossPointFtpServer() {} CrossPointFtpServer::CrossPointFtpServer() {}
@ -52,15 +48,15 @@ void CrossPointFtpServer::begin() {
} }
// Initialize FTP server with credentials // Initialize FTP server with credentials
ftpServer->begin(FTP_USERNAME, FTP_PASSWORD); ftpServer->begin(SETTINGS.ftpUsername.c_str(), SETTINGS.ftpPassword.c_str());
running = true; running = true;
Serial.printf("[%lu] [FTP] FTP server started on port 21\n", millis()); Serial.printf("[%lu] [FTP] FTP server started on port 21\n", millis());
// Show the correct IP based on network mode // Show the correct IP based on network mode
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString(); const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
Serial.printf("[%lu] [FTP] Access at ftp://%s/\n", millis(), ipAddr.c_str()); Serial.printf("[%lu] [FTP] Access at ftp://%s/\n", millis(), ipAddr.c_str());
Serial.printf("[%lu] [FTP] Username: %s\n", millis(), FTP_USERNAME); Serial.printf("[%lu] [FTP] Username: %s\n", millis(), SETTINGS.ftpUsername.c_str());
Serial.printf("[%lu] [FTP] Password: %s\n", millis(), FTP_PASSWORD); Serial.printf("[%lu] [FTP] Password: %s\n", millis(), SETTINGS.ftpPassword.c_str());
Serial.printf("[%lu] [FTP] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [FTP] [MEM] Free heap after server.begin(): %d bytes\n", millis(), ESP.getFreeHeap());
} }

View File

@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include "CrossPointSettings.h"
#include "html/FilesPageHtml.generated.h" #include "html/FilesPageHtml.generated.h"
#include "html/HomePageHtml.generated.h" #include "html/HomePageHtml.generated.h"
@ -150,7 +151,17 @@ void CrossPointWebServer::handleClient() const {
server->handleClient(); server->handleClient();
} }
bool CrossPointWebServer::authenticate() const {
if (!server->authenticate(SETTINGS.httpUsername.c_str(), SETTINGS.httpPassword.c_str())) {
server->requestAuthentication();
Serial.printf("[%lu] [WEB] Authentication failed\n", millis());
return false;
}
return true;
}
void CrossPointWebServer::handleRoot() const { void CrossPointWebServer::handleRoot() const {
if (!authenticate()) return;
server->send(200, "text/html", HomePageHtml); server->send(200, "text/html", HomePageHtml);
Serial.printf("[%lu] [WEB] Served root page\n", millis()); Serial.printf("[%lu] [WEB] Served root page\n", millis());
} }
@ -162,6 +173,7 @@ void CrossPointWebServer::handleNotFound() const {
} }
void CrossPointWebServer::handleStatus() const { void CrossPointWebServer::handleStatus() const {
if (!authenticate()) return;
// Get correct IP based on AP vs STA mode // Get correct IP based on AP vs STA mode
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString(); const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
@ -241,9 +253,13 @@ bool CrossPointWebServer::isEpubFile(const String& filename) const {
return lower.endsWith(".epub"); return lower.endsWith(".epub");
} }
void CrossPointWebServer::handleFileList() const { server->send(200, "text/html", FilesPageHtml); } void CrossPointWebServer::handleFileList() const {
if (!authenticate()) return;
server->send(200, "text/html", FilesPageHtml);
}
void CrossPointWebServer::handleFileListData() const { void CrossPointWebServer::handleFileListData() const {
if (!authenticate()) return;
// Get current path from query string (default to root) // Get current path from query string (default to root)
String currentPath = "/"; String currentPath = "/";
if (server->hasArg("path")) { if (server->hasArg("path")) {
@ -302,6 +318,9 @@ static bool uploadSuccess = false;
static String uploadError = ""; static String uploadError = "";
void CrossPointWebServer::handleUpload() const { void CrossPointWebServer::handleUpload() const {
// Check authentication
if (!authenticate()) return;
static unsigned long lastWriteTime = 0; static unsigned long lastWriteTime = 0;
static unsigned long uploadStartTime = 0; static unsigned long uploadStartTime = 0;
static size_t lastLoggedSize = 0; static size_t lastLoggedSize = 0;
@ -416,6 +435,7 @@ void CrossPointWebServer::handleUpload() const {
} }
void CrossPointWebServer::handleUploadPost() const { void CrossPointWebServer::handleUploadPost() const {
if (!authenticate()) return;
if (uploadSuccess) { if (uploadSuccess) {
server->send(200, "text/plain", "File uploaded successfully: " + uploadFileName); server->send(200, "text/plain", "File uploaded successfully: " + uploadFileName);
} else { } else {
@ -425,6 +445,7 @@ void CrossPointWebServer::handleUploadPost() const {
} }
void CrossPointWebServer::handleCreateFolder() const { void CrossPointWebServer::handleCreateFolder() const {
if (!authenticate()) return;
// Get folder name from form data // Get folder name from form data
if (!server->hasArg("name")) { if (!server->hasArg("name")) {
server->send(400, "text/plain", "Missing folder name"); server->send(400, "text/plain", "Missing folder name");
@ -475,6 +496,7 @@ void CrossPointWebServer::handleCreateFolder() const {
} }
void CrossPointWebServer::handleDelete() const { void CrossPointWebServer::handleDelete() const {
if (!authenticate()) return;
// Get path from form data // Get path from form data
if (!server->hasArg("path")) { if (!server->hasArg("path")) {
server->send(400, "text/plain", "Missing path"); server->send(400, "text/plain", "Missing path");

View File

@ -38,6 +38,9 @@ class CrossPointWebServer {
bool apMode = false; // true when running in AP mode, false for STA mode bool apMode = false; // true when running in AP mode, false for STA mode
uint16_t port = 80; uint16_t port = 80;
// Authentication helper
bool authenticate() const;
// File scanning // File scanning
void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const; void scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const;
String formatFileSize(size_t bytes) const; String formatFileSize(size_t bytes) const;