🖥️ VisionFive2 BMC Dashboard
@@ -717,6 +816,122 @@
// API base URL
const API_BASE = '';
+ // Authentication
+ let authCredentials = null;
+
+ // Get stored credentials from sessionStorage
+ function getStoredCredentials() {
+ const stored = sessionStorage.getItem('bmc_auth');
+ if (stored) {
+ return JSON.parse(stored);
+ }
+ return null;
+ }
+
+ // Store credentials in sessionStorage
+ function storeCredentials(username, password) {
+ sessionStorage.setItem('bmc_auth', JSON.stringify({ username, password }));
+ }
+
+ // Clear stored credentials
+ function clearCredentials() {
+ sessionStorage.removeItem('bmc_auth');
+ authCredentials = null;
+ }
+
+ // Create Basic Auth header value
+ function getAuthHeader() {
+ if (!authCredentials) {
+ const stored = getStoredCredentials();
+ if (stored) {
+ authCredentials = stored;
+ } else {
+ return null;
+ }
+ }
+ return 'Basic ' + btoa(authCredentials.username + ':' + authCredentials.password);
+ }
+
+ // Handle login form submission
+ async function handleLogin(event) {
+ event.preventDefault();
+ const username = document.getElementById('username').value;
+ const password = document.getElementById('password').value;
+ const errorDiv = document.getElementById('login-error');
+
+ // Store credentials temporarily
+ authCredentials = { username, password };
+
+ // Test credentials by making an API call
+ try {
+ const authHeader = 'Basic ' + btoa(username + ':' + password);
+ const response = await fetch(`${API_BASE}/api/system/info`, {
+ headers: {
+ 'Authorization': authHeader
+ }
+ });
+
+ if (response.status === 401) {
+ errorDiv.textContent = 'Invalid username or password';
+ authCredentials = null;
+ return false;
+ }
+
+ if (!response.ok) {
+ errorDiv.textContent = 'Connection error. Please try again.';
+ authCredentials = null;
+ return false;
+ }
+
+ // Success - store credentials and show main content
+ storeCredentials(username, password);
+ document.getElementById('login-overlay').classList.add('hidden');
+ document.getElementById('main-container').style.display = 'block';
+
+ // Initialize the dashboard
+ initialize();
+
+ } catch (error) {
+ errorDiv.textContent = 'Connection error. Please try again.';
+ authCredentials = null;
+ return false;
+ }
+
+ return false;
+ }
+
+ // Check if already logged in
+ function checkAuth() {
+ const stored = getStoredCredentials();
+ if (stored) {
+ authCredentials = stored;
+ // Verify credentials are still valid
+ const authHeader = getAuthHeader();
+ fetch(`${API_BASE}/api/system/info`, {
+ headers: { 'Authorization': authHeader }
+ })
+ .then(response => {
+ if (response.status === 401) {
+ clearCredentials();
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ } else {
+ document.getElementById('login-overlay').classList.add('hidden');
+ document.getElementById('main-container').style.display = 'block';
+ initialize();
+ }
+ })
+ .catch(() => {
+ // On error, still show login
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ });
+ } else {
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ }
+ }
+
// Toast notification
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
@@ -730,8 +945,9 @@
}, 3000);
}
- // API helper
+ // API helper with authentication
async function apiCall(endpoint, method = 'GET', data = null) {
+ const authHeader = getAuthHeader();
const options = {
method: method,
headers: {
@@ -739,12 +955,25 @@
}
};
+ if (authHeader) {
+ options.headers['Authorization'] = authHeader;
+ }
+
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(`${API_BASE}${endpoint}`, options);
+
+ // Handle 401 - session expired
+ if (response.status === 401) {
+ clearCredentials();
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ throw new Error('Session expired. Please login again.');
+ }
+
const result = await response.json();
if (!result.success) {
@@ -1168,6 +1397,11 @@
showToast('Firmware uploaded successfully! Device will restart...', 'success');
document.getElementById('ota-progress-bar').style.width = '100%';
document.getElementById('ota-progress-text').textContent = '100%';
+ } else if (xhr.status === 401) {
+ showToast('Session expired. Please login again.', 'error');
+ clearCredentials();
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
} else {
let errorMsg = 'Upload failed';
try {
@@ -1187,6 +1421,13 @@
};
xhr.open('POST', `${API_BASE}/api/ota/upload`);
+
+ // Add authentication header
+ const authHeader = getAuthHeader();
+ if (authHeader) {
+ xhr.setRequestHeader('Authorization', authHeader);
+ }
+
xhr.send(formData);
} catch (error) {
@@ -1197,8 +1438,8 @@
}
}
- // Initialize
- document.addEventListener('DOMContentLoaded', () => {
+ // Initialize function called after successful login
+ function initialize() {
updateSystemInfo();
updateGPIOStates();
updatePowerStatus();
@@ -1243,6 +1484,11 @@
setInterval(updateGPIOStates, 2000);
setInterval(updatePowerStatus, 2000);
setInterval(updateOTAStatus, 5000);
+ }
+
+ // Check authentication on page load
+ document.addEventListener('DOMContentLoaded', () => {
+ checkAuth();
});
// Log viewer functions
@@ -1250,7 +1496,18 @@
async function refreshLogs() {
try {
- const response = await fetch(`${API_BASE}/api/logs`);
+ const authHeader = getAuthHeader();
+ const response = await fetch(`${API_BASE}/api/logs`, {
+ headers: authHeader ? { 'Authorization': authHeader } : {}
+ });
+
+ if (response.status === 401) {
+ clearCredentials();
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ return;
+ }
+
const logs = await response.text();
const logsOutput = document.getElementById('logs-output');
logsOutput.innerHTML = `
#include
@@ -66,6 +67,122 @@ static esp_err_t send_json_success(httpd_req_t *req, cJSON *data) {
return ret;
}
+// ============================================================================
+// HTTP Basic Authentication
+// ============================================================================
+
+#if BMC_HTTP_AUTH_ENABLED
+
+/**
+ * @brief Check if request has valid authentication
+ *
+ * @param req HTTP request
+ * @return true if authenticated, false otherwise
+ */
+static bool check_auth(httpd_req_t *req) {
+ char auth_header[128];
+ bool authenticated = false;
+
+ // Method 1: Check Authorization header
+ esp_err_t ret = httpd_req_get_hdr_value_str(req, "Authorization", auth_header, sizeof(auth_header));
+ if (ret == ESP_OK && strncmp(auth_header, "Basic ", 6) == 0) {
+ // Decode base64 credentials
+ const char *b64_credentials = auth_header + 6;
+ size_t b64_len = strlen(b64_credentials);
+
+ unsigned char decoded[64];
+ size_t decoded_len = 0;
+
+ int dec_ret = mbedtls_base64_decode(decoded, sizeof(decoded) - 1, &decoded_len,
+ (const unsigned char *)b64_credentials, b64_len);
+ if (dec_ret == 0) {
+ decoded[decoded_len] = '\0';
+
+ // Build expected credentials string "username:password"
+ char expected_creds[64];
+ snprintf(expected_creds, sizeof(expected_creds), "%s:%s", BMC_HTTP_AUTH_USERNAME, BMC_HTTP_AUTH_PASSWORD);
+
+ authenticated = (strcmp((char *)decoded, expected_creds) == 0);
+ }
+ }
+
+ // Method 2: Check query parameter "auth" for WebSocket support
+ // Format: ?auth=base64(username:password)
+ if (!authenticated) {
+ char query[256];
+ ret = httpd_req_get_url_query_str(req, query, sizeof(query));
+ if (ret == ESP_OK) {
+ char auth_param[128];
+ ret = httpd_query_key_value(query, "auth", auth_param, sizeof(auth_param));
+ if (ret == ESP_OK) {
+ // Decode base64 credentials from query parameter
+ size_t b64_len = strlen(auth_param);
+ unsigned char decoded[64];
+ size_t decoded_len = 0;
+
+ int dec_ret = mbedtls_base64_decode(decoded, sizeof(decoded) - 1, &decoded_len,
+ (const unsigned char *)auth_param, b64_len);
+ if (dec_ret == 0) {
+ decoded[decoded_len] = '\0';
+
+ // Build expected credentials string "username:password"
+ char expected_creds[64];
+ snprintf(expected_creds, sizeof(expected_creds), "%s:%s", BMC_HTTP_AUTH_USERNAME,
+ BMC_HTTP_AUTH_PASSWORD);
+
+ authenticated = (strcmp((char *)decoded, expected_creds) == 0);
+ }
+ }
+ }
+ }
+
+ return authenticated;
+}
+
+/**
+ * @brief Send 401 Unauthorized response
+ *
+ * @param req HTTP request
+ * @return ESP_OK
+ */
+static esp_err_t send_auth_required(httpd_req_t *req) {
+ char www_auth[64];
+ snprintf(www_auth, sizeof(www_auth), "Basic realm=\"%s\"", BMC_HTTP_AUTH_REALM);
+ httpd_resp_set_hdr(req, "WWW-Authenticate", www_auth);
+ httpd_resp_set_status(req, "401 Unauthorized");
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_sendstr(req, "{\"success\":false,\"error\":\"Authentication required\"}");
+ return ESP_OK;
+}
+
+/**
+ * @brief Authentication middleware wrapper for handlers
+ *
+ * @param req HTTP request
+ * @param handler The actual handler to call if authenticated
+ * @return ESP_OK on success, error otherwise
+ */
+static esp_err_t auth_wrapper(httpd_req_t *req, esp_err_t (*handler)(httpd_req_t *)) {
+ if (!check_auth(req)) {
+ return send_auth_required(req);
+ }
+ return handler(req);
+}
+
+// Macro to create authenticated handler wrapper
+#define AUTH_HANDLER(name, handler_func) \
+ static esp_err_t name(httpd_req_t *req) { \
+ return auth_wrapper(req, handler_func); \
+ }
+
+#else
+// No authentication - pass through directly
+#define AUTH_HANDLER(name, handler_func) \
+ static esp_err_t name(httpd_req_t *req) { \
+ return handler_func(req); \
+ }
+#endif
+
// ============================================================================
// Static File Handlers
// ============================================================================
@@ -387,6 +504,10 @@ static void uart_to_ws_callback(const uint8_t *data, size_t len) {
}
static esp_err_t ws_serial_handler(httpd_req_t *req) {
+ // Note: WebSocket authentication is disabled because browsers don't support
+ // custom Authorization headers on WebSocket connections.
+ // TODO: Find a way to fix this
+
// For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake
// internally. This handler is only called for frame processing.
// Client tracking is done dynamically in the broadcast function.
@@ -733,70 +854,90 @@ static esp_err_t api_ota_upload_handler(httpd_req_t *req) {
// URI Registration
// ============================================================================
+// Authenticated handler wrappers
+AUTH_HANDLER(auth_index_handler, index_handler)
+AUTH_HANDLER(auth_api_gpio_get_all_handler, api_gpio_get_all_handler)
+AUTH_HANDLER(auth_api_gpio_set_handler, api_gpio_set_handler)
+AUTH_HANDLER(auth_api_gpio_config_handler, api_gpio_config_handler)
+AUTH_HANDLER(auth_api_power_status_handler, api_power_status_handler)
+AUTH_HANDLER(auth_api_power_on_handler, api_power_on_handler)
+AUTH_HANDLER(auth_api_power_off_handler, api_power_off_handler)
+AUTH_HANDLER(auth_api_power_reset_handler, api_power_reset_handler)
+AUTH_HANDLER(auth_api_serial_config_get_handler, api_serial_config_get_handler)
+AUTH_HANDLER(auth_api_serial_config_set_handler, api_serial_config_set_handler)
+AUTH_HANDLER(auth_api_serial_send_handler, api_serial_send_handler)
+AUTH_HANDLER(auth_api_system_info_handler, api_system_info_handler)
+AUTH_HANDLER(auth_api_system_status_handler, api_system_status_handler)
+AUTH_HANDLER(auth_api_ota_status_handler, api_ota_status_handler)
+AUTH_HANDLER(auth_api_ota_update_handler, api_ota_update_handler)
+AUTH_HANDLER(auth_api_ota_upload_handler, api_ota_upload_handler)
+AUTH_HANDLER(auth_api_logs_get_handler, api_logs_get_handler)
+AUTH_HANDLER(auth_api_logs_clear_handler, api_logs_clear_handler)
+
static const httpd_uri_t uri_index = {
.uri = "/",
.method = HTTP_GET,
- .handler = index_handler,
+ .handler = auth_index_handler,
};
static const httpd_uri_t uri_api_gpio = {
.uri = "/api/gpio",
.method = HTTP_GET,
- .handler = api_gpio_get_all_handler,
+ .handler = auth_api_gpio_get_all_handler,
};
static const httpd_uri_t uri_api_gpio_set = {
.uri = "/api/gpio/set",
.method = HTTP_POST,
- .handler = api_gpio_set_handler,
+ .handler = auth_api_gpio_set_handler,
};
static const httpd_uri_t uri_api_gpio_config = {
.uri = "/api/gpio/config",
.method = HTTP_PUT,
- .handler = api_gpio_config_handler,
+ .handler = auth_api_gpio_config_handler,
};
static const httpd_uri_t uri_api_power_status = {
.uri = "/api/power/status",
.method = HTTP_GET,
- .handler = api_power_status_handler,
+ .handler = auth_api_power_status_handler,
};
static const httpd_uri_t uri_api_power_on = {
.uri = "/api/power/on",
.method = HTTP_POST,
- .handler = api_power_on_handler,
+ .handler = auth_api_power_on_handler,
};
static const httpd_uri_t uri_api_power_off = {
.uri = "/api/power/off",
.method = HTTP_POST,
- .handler = api_power_off_handler,
+ .handler = auth_api_power_off_handler,
};
static const httpd_uri_t uri_api_power_reset = {
.uri = "/api/power/reset",
.method = HTTP_POST,
- .handler = api_power_reset_handler,
+ .handler = auth_api_power_reset_handler,
};
static const httpd_uri_t uri_api_serial_config_get = {
.uri = "/api/serial/config",
.method = HTTP_GET,
- .handler = api_serial_config_get_handler,
+ .handler = auth_api_serial_config_get_handler,
};
static const httpd_uri_t uri_api_serial_config_set = {
.uri = "/api/serial/config",
.method = HTTP_PUT,
- .handler = api_serial_config_set_handler,
+ .handler = auth_api_serial_config_set_handler,
};
static const httpd_uri_t uri_api_serial_send = {
.uri = "/api/serial/send",
.method = HTTP_POST,
- .handler = api_serial_send_handler,
+ .handler = auth_api_serial_send_handler,
};
static const httpd_uri_t uri_ws_serial = {
@@ -809,43 +950,43 @@ static const httpd_uri_t uri_ws_serial = {
static const httpd_uri_t uri_api_system_info = {
.uri = "/api/system/info",
.method = HTTP_GET,
- .handler = api_system_info_handler,
+ .handler = auth_api_system_info_handler,
};
static const httpd_uri_t uri_api_system_status = {
.uri = "/api/system/status",
.method = HTTP_GET,
- .handler = api_system_status_handler,
+ .handler = auth_api_system_status_handler,
};
static const httpd_uri_t uri_api_ota_status = {
.uri = "/api/ota/status",
.method = HTTP_GET,
- .handler = api_ota_status_handler,
+ .handler = auth_api_ota_status_handler,
};
static const httpd_uri_t uri_api_ota_update = {
.uri = "/api/ota/update",
.method = HTTP_POST,
- .handler = api_ota_update_handler,
+ .handler = auth_api_ota_update_handler,
};
static const httpd_uri_t uri_api_ota_upload = {
.uri = "/api/ota/upload",
.method = HTTP_POST,
- .handler = api_ota_upload_handler,
+ .handler = auth_api_ota_upload_handler,
};
static const httpd_uri_t uri_api_logs_get = {
.uri = "/api/logs",
.method = HTTP_GET,
- .handler = api_logs_get_handler,
+ .handler = auth_api_logs_get_handler,
};
static const httpd_uri_t uri_api_logs_clear = {
.uri = "/api/logs/clear",
.method = HTTP_POST,
- .handler = api_logs_clear_handler,
+ .handler = auth_api_logs_clear_handler,
};
// ============================================================================
${escapeHtml(logs)}`;
@@ -1264,7 +1521,19 @@
async function clearLogs() {
try {
- await fetch(`${API_BASE}/api/logs/clear`, { method: 'POST' });
+ const authHeader = getAuthHeader();
+ const response = await fetch(`${API_BASE}/api/logs/clear`, {
+ method: 'POST',
+ headers: authHeader ? { 'Authorization': authHeader } : {}
+ });
+
+ if (response.status === 401) {
+ clearCredentials();
+ document.getElementById('login-overlay').classList.remove('hidden');
+ document.getElementById('main-container').style.display = 'none';
+ return;
+ }
+
refreshLogs();
showToast('Logs cleared', 'success');
} catch (error) {
diff --git a/main/web_server.c b/main/web_server.c
index 964d40d..38d2b38 100644
--- a/main/web_server.c
+++ b/main/web_server.c
@@ -7,6 +7,7 @@
#include "log_handler.h"
#include "esp_log.h"
#include "cJSON.h"
+#include "mbedtls/base64.h"
#include